package gardener

import (
	"context"
	"crypto/tls"
	"encoding/json"
	stderrors "errors"
	"fmt"
	"net"
	"net/http"
	"reflect"
	"sort"
	"strconv"
	"strings"
	"sync"
	"time"

	pcaissuerv1beta1 "github.com/cert-manager/aws-privateca-issuer/pkg/api/v1beta1"
	certmanagerv1alpha2 "github.com/jetstack/cert-manager/pkg/apis/certmanager/v1alpha2"
	"github.com/pingcap/errors"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	"golang.org/x/sync/errgroup"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/types"
	utilruntime "k8s.io/apimachinery/pkg/util/runtime"
	clientgoscheme "k8s.io/client-go/kubernetes/scheme"
	"k8s.io/client-go/rest"
	"k8s.io/client-go/tools/clientcmd"
	"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/manager"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/consts"
	awsv1alpha1 "github.com/tidbcloud/infra-api/pkg/gardener/aws/apis/v1alpha1"
	gardencorev1beta1 "github.com/tidbcloud/infra-api/pkg/gardener/core/apis/v1beta1"
	gcpv1alpha1 "github.com/tidbcloud/infra-api/pkg/gardener/gcp/apis/v1alpha1"
	"github.com/tidbcloud/infra-api/pkg/nodegroup"
	asts "github.com/tidbcloud/infra-api/pkg/pingcap/apis/v1"
)

var (
	ErrShootNotReady           = stderrors.New("shoot not ready")
	ErrShootAlreadyHibernation = stderrors.New("shoot already hibernation")
	ErrShootWakingUp           = stderrors.New("shoot is waking up")
)

const (
	NodeGroupSetOwnerKey       = "cloud.pingcap.com/owner"
	shootContext_build_timeout = 5

	defaultShootAPIQPS   = 50
	defaultShootAPIBurst = 100

	// MasterWorkerGroup is gardener master group name
	MasterWorkerGroup = "master"
	// AwakeTimeAnnotation is annotation that record last awake time
	AwakeTimeAnnotation = "tidbcloud.com/awake-time"
)

var Scheme = runtime.NewScheme()

func init() {
	utilruntime.Must(clientgoscheme.AddToScheme(Scheme))
	utilruntime.Must(gardencorev1beta1.AddToScheme(Scheme))
	utilruntime.Must(certmanagerv1alpha2.AddToScheme(Scheme))
	utilruntime.Must(pcaissuerv1beta1.AddToScheme(Scheme))
	utilruntime.Must(asts.AddToScheme(Scheme))
}

type ShootContext struct {
	Manager    manager.Manager
	Kubeconfig []byte

	cancel context.CancelFunc

	PDMixedDeployConfig   *cloudv1.MixedDeployPDWithTiKV
	TiFlashSingleAZDeploy bool
}

type ShootReconcilerRegister interface {
	Regist(manager.Manager, *gardencorev1beta1.Shoot, GardenerAPI) error
}

// GardEnvironment struct define
type GardEnvironment struct {
	projectName string
	baseDomain  string

	config              *cloudv1.Gardener
	PDMixedDeployConfig *cloudv1.MixedDeployPDWithTiKV
	featureGates        map[cloudv1.FeatureGateName]cloudv1.FeatureGate

	client client.Client
	mgr    manager.Manager

	// shootReconcilers are reconcilers that should be registered to shoot
	shootReconcilers []ShootReconcilerRegister

	shootMu sync.RWMutex
	shoots  map[string]*ShootContext
	started bool
	group   *errgroup.Group
	ctx     context.Context
	cancel  context.CancelFunc
}

func NewGardEnvironment(config *cloudv1.Gardener, infraapiConfig *cloudv1.InfraAPIConfig,
	reconcilers []ShootReconcilerRegister, restConfig ...*rest.Config) (env *GardEnvironment, err error) {

	var cfg *rest.Config
	if len(restConfig) > 0 {
		cfg = restConfig[0]
	} else {
		if config.KubeConfigPath == "" {
			cfg, err = rest.InClusterConfig()
			if err != nil {
				return nil, errors.AddStack(err)
			}
		} else {
			cfg, err = clientcmd.BuildConfigFromFlags("", config.KubeConfigPath)
			if err != nil {
				return nil, errors.AddStack(err)
			}
		}
	}

	mgr, err := manager.New(cfg, manager.Options{Scheme: Scheme, LeaderElection: false, MetricsBindAddress: "0", Namespace: fmt.Sprintf("garden-%s", config.GardenerProjectName)})
	if err != nil {
		return nil, errors.AddStack(err)
	}

	client := mgr.GetClient()

	return &GardEnvironment{
		projectName:         config.GardenerProjectName,
		baseDomain:          config.GardenBaseDomain,
		config:              config,
		PDMixedDeployConfig: &infraapiConfig.MixedDeployPDWithTiKV,
		featureGates:        infraapiConfig.FeatureGates,
		shoots:              make(map[string]*ShootContext),
		client:              client,
		mgr:                 mgr,

		shootReconcilers: reconcilers,
	}, nil
}

func (g *GardEnvironment) ProjectName() string {
	return g.projectName
}

func (g *GardEnvironment) startManager(ctx context.Context, mgr manager.Manager) (cancel context.CancelFunc) {
	ctx, cancel = context.WithCancel(ctx)
	g.group.Go(func() error {
		return mgr.Start(ctx)
	})
	return
}

// Start start to run all manager.
// Start blocks and return when all managers stop to run.
// it will cancel running if any manager failed to start.
func (g *GardEnvironment) Start(ctx context.Context) error {
	err := g.start(ctx)
	if err != nil {
		return err
	}

	return g.group.Wait()
}

func (g *GardEnvironment) start(ctx context.Context) error {
	g.shootMu.Lock()
	defer g.shootMu.Unlock()

	if g.started {
		return errors.Errorf("already started")
	}

	g.started = true
	g.ctx, g.cancel = context.WithCancel(ctx)
	var firstErrCtx context.Context
	g.group, firstErrCtx = errgroup.WithContext(ctx)

	// cancel all manager when any one meet error
	go func() {
		<-firstErrCtx.Done()
		g.cancel()
	}()

	g.startManager(g.ctx, g.mgr)

	for _, s := range g.shoots {
		s.cancel = g.startManager(g.ctx, s.Manager)
	}

	return nil
}

// Manager return the manager against the gardener API.
// You can set up controller using this manager.
func (g *GardEnvironment) Manager() manager.Manager {
	return g.mgr
}

func (g *GardEnvironment) getShootContext(name string) (sctx *ShootContext, ok bool) {
	g.shootMu.RLock()
	defer g.shootMu.RUnlock()

	sctx, ok = g.shoots[name]
	return
}

func (g *GardEnvironment) GetShootContextWithNamespace(ctx context.Context, name string, namespace string) (sctx *ShootContext, err error) {
	if sctx, ok := g.getShootContext(name); ok {
		return sctx, nil
	}

	g.shootMu.Lock()
	defer g.shootMu.Unlock()

	sctx, err = g.buildShootCtxWithNamespace(ctx, name, namespace)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil, ErrShootNotReady
		}
		return nil, err
	}

	if g.started {
		sctx.cancel = g.startManager(g.ctx, sctx.Manager)
		// we start the manager async, wait for the cache to be started in side `Manager.Start()`
		// or if user use the Manager.GetClient() right away will meet error: "the cache is not started, can not read objects"
		<-sctx.Manager.Elected()
	}

	g.shoots[name] = sctx
	return
}

// GetShootContext return the shoot context when the shoot is ready.
// return ErrShootNotReady if the shoot is not ready(we can not get the kubeconfig to connect yet)
func (g *GardEnvironment) GetShootContext(ctx context.Context, name string) (*ShootContext, error) {
	var (
		sctx *ShootContext
		err  error
	)
	if sctx, ok := g.getShootContext(name); ok {
		return sctx, nil
	}

	g.shootMu.Lock()
	defer g.shootMu.Unlock()

	shoot, err := g.GetShoot(ctx, name)
	if err != nil {
		return nil, err
	}

	// if shoot already hibernated, build shoot context will failed
	if EnableHibernation(shoot) {
		return nil, ErrShootAlreadyHibernation
	}
	if IsWakingUp(shoot) {
		return nil, ErrShootWakingUp
	}

	sctx, err = g.buildShootCtx(ctx, name)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return nil, ErrShootNotReady
		}
		return nil, err
	}

	if g.started {
		sctx.cancel = g.startManager(g.ctx, sctx.Manager)
		// we start the manager async, wait for the cache to be started in side `Manager.Start()`
		// or if user use the Manager.GetClient() right away will meet error: "the cache is not started, can not read objects"
		<-sctx.Manager.Elected()
	}

	g.shoots[name] = sctx
	return sctx, nil
}

// Close GardEnvironment.
func (g *GardEnvironment) Close() {
	g.cancel()
}

// GetShoot get a shoot
func (g *GardEnvironment) ShootClusterIsExist(ctx context.Context, name string) (bool, error) {
	var shoot gardencorev1beta1.Shoot
	err := g.client.Get(ctx, types.NamespacedName{Namespace: g.GetShootNamespace(), Name: name}, &shoot)
	if err != nil {
		if apierrors.IsNotFound(err) {
			return false, nil
		}
		return false, err
	}

	return true, nil
}

func (g *GardEnvironment) buildShootCtx(ctx context.Context, name string) (sctx *ShootContext, err error) {
	cfg, kubeconfig, err := g.getShootRestConfig(ctx, name)
	if err != nil {
		return nil, err
	}

	cfg.QPS = defaultShootAPIQPS
	cfg.Burst = defaultShootAPIBurst

	mgr, err := manager.New(cfg, manager.Options{Scheme: Scheme, LeaderElection: false, MetricsBindAddress: "0"})
	if err != nil {
		return nil, errors.AddStack(err)
	}

	err = v1alpha1.AddToScheme(mgr.GetScheme())
	if err != nil {
		return nil, errors.AddStack(err)
	}

	shootSpec, err := g.GetShoot(ctx, name)
	if err != nil {
		return nil, errors.Annotate(err, "GetShoot")
	}

	for _, reconciler := range g.shootReconcilers {
		if err := reconciler.Regist(mgr, shootSpec, g); err != nil {
			return nil, errors.AddStack(err)
		}
	}
	tenantID := GetTenantIDFromLabels(shootSpec.Labels)
	projectID := GetProjectIDFromLabels(shootSpec.Labels)
	tiflashSingleAZDeploy := cloudv1.FeatureEnabled(g.featureGates, cloudv1.TiFlashSingleAZ, tenantID, projectID)
	shoot := &ShootContext{
		Manager:               mgr,
		Kubeconfig:            kubeconfig,
		PDMixedDeployConfig:   g.PDMixedDeployConfig,
		TiFlashSingleAZDeploy: tiflashSingleAZDeploy,
	}

	return shoot, nil
}

func GetTenantIDFromLabels(labels map[string]string) uint64 {
	if tid, ok := labels["tenant"]; ok {
		if uid, err := strconv.ParseUint(tid, 10, 64); err == nil {
			return uid
		}
	}
	return 0
}

func GetProjectIDFromLabels(labels map[string]string) uint64 {
	if pid, ok := labels["project"]; ok {
		if uid, err := strconv.ParseUint(pid, 10, 64); err == nil {
			return uid
		}
	}
	return 0
}

func (g *GardEnvironment) buildShootCtxWithNamespace(ctx context.Context, name string, namespace string) (sctx *ShootContext, err error) {
	cfg, kubeconfig, err := g.getShootRestConfigWithNamespace(ctx, name, namespace)
	if err != nil {
		return nil, err
	}

	return g.getShootCtxWithKubeconfig(cfg, kubeconfig)
}

// TODO: optimize the function name
func (g *GardEnvironment) getShootCtxWithKubeconfig(cfg *rest.Config, kubeconfig []byte) (shoot *ShootContext, err error) {
	// Designed for migration
	// test connectivity for the client to avoid invalid shoot config error log
	if _, err := net.LookupHost(strings.TrimPrefix(cfg.Host, "https://")); err != nil {
		return nil, err
	}

	testConnectivityClient := &http.Client{
		Transport: &http.Transport{
			/* #nosec */
			TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
		},
		Timeout: shootContext_build_timeout * time.Second,
	}

	if _, err = testConnectivityClient.Head(cfg.Host); err != nil {
		return nil, err
	}

	// TODO: fix concurrent map write panic
	mgr, err := manager.New(cfg, manager.Options{Scheme: Scheme, LeaderElection: false, MetricsBindAddress: "0"})
	if err != nil {
		return nil, errors.AddStack(err)
	}

	err = v1alpha1.AddToScheme(mgr.GetScheme())
	if err != nil {
		return nil, errors.AddStack(err)
	}

	shoot = &ShootContext{
		Manager:             mgr,
		Kubeconfig:          kubeconfig,
		PDMixedDeployConfig: g.PDMixedDeployConfig,
	}

	return shoot, err
}

func (g *GardEnvironment) GetShootNamespace() string {
	return fmt.Sprintf("garden-%s", g.projectName)
}

func (g *GardEnvironment) ListShoots(ctx context.Context) (*gardencorev1beta1.ShootList, error) {
	shoots := gardencorev1beta1.ShootList{}

	// remove client.InNamespace(g.GetShootNamespace()) and the same function in LoadShootClientsIntoCache,
	// we can load all the resources in shoot.
	if err := g.client.List(ctx, &shoots, client.InNamespace(g.GetShootNamespace())); err != nil {
		return nil, err
	}

	return &shoots, nil
}

// TODO: check if this function could be a private
// LoadShootClientsIntoCache cache shoot clients in batch, working for migration job.
func (g *GardEnvironment) LoadShootClientsIntoCache(ctx context.Context) error {
	var wg sync.WaitGroup

	secrets := corev1.SecretList{}

	// TODO: limit the gardener project, use the correct project name from config
	if err := g.client.List(ctx, &secrets, client.InNamespace(g.GetShootNamespace())); err != nil {
		return err
	}

	for _, secret := range secrets.Items {
		if !strings.Contains(secret.Name, "kubeconfig") {
			continue
		}

		shootName := strings.TrimRight(secret.Name, ".kubeconfig")

		// If shootCtx exist, skip get shoot context
		if _, ok := g.getShootContext(shootName); ok {
			continue
		}
		kubeconfig := secret.Data["kubeconfig"]

		wg.Add(1)
		// TODO: handle error
		go func() {
			defer wg.Done()
			cfg, err := clientcmd.NewClientConfigFromBytes(kubeconfig)
			if err != nil {
				// TODO: handle error
				return
			}

			restConfig, err := cfg.ClientConfig()
			if err != nil {
				// TODO: handle error
				return
			}

			shoot, err := g.getShootCtxWithKubeconfig(restConfig, kubeconfig)
			if err != nil {
				// TODO: handle error
				return
			}

			if g.started {
				shoot.cancel = g.startManager(g.ctx, shoot.Manager)
				// we start the manager async, wait for the cache to be started in side `Manager.Start()`
				// or if user use the Manager.GetClient() right away will meet error: "the cache is not started, can not read objects"
				<-shoot.Manager.Elected()
			}

			g.shootMu.Lock()
			g.shoots[shootName] = shoot
			g.shootMu.Unlock()
		}()
	}
	wg.Wait()

	return nil
}

func (g *GardEnvironment) getShootRestConfig(ctx context.Context, name string) (*rest.Config, []byte, error) {
	return g.getShootRestConfigWithNamespace(ctx, name, g.GetShootNamespace())
}

func (g *GardEnvironment) getShootRestConfigWithNamespace(ctx context.Context, name string, namespace string) (*rest.Config, []byte, error) {
	cfg, kubeconfig, err := g.getShootClientConfig(ctx, namespace, name)
	if err != nil {
		return nil, nil, err
	}

	restConfig, err := cfg.ClientConfig()
	if err != nil {
		return nil, nil, err
	}

	return restConfig, kubeconfig, nil
}

func (g *GardEnvironment) getShootClientConfig(ctx context.Context, namespace string, shootName string) (clientConfig clientcmd.ClientConfig, kubeconfig []byte, err error) {
	secretName := fmt.Sprintf("%s.kubeconfig", shootName)
	var secret corev1.Secret
	err = g.client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: secretName}, &secret)
	if err != nil {
		return nil, nil, err
	}

	data := secret.Data["kubeconfig"]
	clientConfig, err = clientcmd.NewClientConfigFromBytes(data)
	if err != nil {
		return nil, nil, err
	}

	return clientConfig, data, nil
}

// CreateOrUpdateShoot create the shoot if it not exist.
// labels will also be added to the master worker group.
// if the shoot exist, 'master' Worker will be update.
func (g *GardEnvironment) CreateOrUpdateShoot(
	ctx context.Context,
	s *ShootArgs,
) (controllerutil.OperationResult, error) {
	masterWG, err := buildMasterGroup(s.ProviderType, s.Zones, s.Labels, g.config)
	if err != nil {
		return controllerutil.OperationResultNone, err
	}
	workers := []gardencorev1beta1.Worker{*masterWG}

	shoot, err := g.buildShoot(s.Name, s.ProviderType, s.Region, s.Zones, workers, s.Labels, s.Cidr, s.WorkerCidr, s.PublicLBCidr, s.InternalLBCidr)
	if err != nil {
		return controllerutil.OperationResultNone, err
	}

	return controllerutil.CreateOrUpdate(ctx, g.client, shoot, func() error {
		for i := 0; i < len(shoot.Spec.Provider.Workers); i++ {
			if shoot.Spec.Provider.Workers[i].Name == masterWG.Name {
				shoot.Spec.Provider.Workers[i].Maximum = masterWG.Maximum
				shoot.Spec.Provider.Workers[i].Minimum = masterWG.Minimum
			}
		}
		return nil
	})
}

func (g *GardEnvironment) UpdateShootFirewall(ctx context.Context, shootName string, cidrs []string) error {
	shoot, err := g.GetShoot(ctx, shootName)
	if err != nil {
		return err
	}
	shootPatch := client.MergeFrom(shoot.DeepCopy())
	shoot.Spec.LoadBalancerSourceRanges = cidrs
	return g.client.Patch(ctx, shoot, shootPatch)
}

func (g *GardEnvironment) GetShoot(ctx context.Context, shootName string) (*gardencorev1beta1.Shoot, error) {
	shoot := &gardencorev1beta1.Shoot{}

	if err := g.client.Get(ctx, types.NamespacedName{Namespace: g.GetShootNamespace(), Name: shootName}, shoot); err != nil {
		return nil, err
	}

	return shoot, nil
}

// ApplyNodeGroupSet apply a Set to the target network (identified by shootName), with the following behavior:
// 1. NodeGroup in the set but not exist in the network will be created
// 2. NodeGroup in the set and exist in the network will be updated
// 3. NodeGroup not in the set but exist in the network will be deleted
func (g *GardEnvironment) ApplyNodeGroupSet(ctx context.Context, shootName string, ngSet *nodegroup.Set, policy NodeGroupPolicy) ([]NodeGroupChange, error) {
	if len(ngSet.Name) < 1 {
		return nil, errors.New("Set name must not be empty")
	}
	shoot := new(gardencorev1beta1.Shoot)
	err := g.client.Get(ctx, types.NamespacedName{Namespace: g.GetShootNamespace(), Name: shootName}, shoot)
	if err != nil {
		return nil, err
	}
	// preprocess: adopt the orphans
	for i := range shoot.Spec.Provider.Workers {
		// already owned by a Set, skip
		if _, ok := shoot.Spec.Provider.Workers[i].Labels[NodeGroupSetOwnerKey]; ok {
			continue
		}
		for _, managed := range ngSet.NodeGroups {
			if shoot.Spec.Provider.Workers[i].Name == managed.Name {
				// the orphan should be managed by the current Set, adopt it
				if shoot.Spec.Provider.Workers[i].Labels == nil {
					shoot.Spec.Provider.Workers[i].Labels = map[string]string{}
				}
				shoot.Spec.Provider.Workers[i].Labels[NodeGroupSetOwnerKey] = ngSet.Name
			}
		}
	}

	var allNodeGroups []gardencorev1beta1.Worker
	var changes []NodeGroupChange
	existing := map[string]gardencorev1beta1.Worker{}

	for _, ng := range shoot.Spec.Provider.Workers {
		if ng.Labels[NodeGroupSetOwnerKey] != ngSet.Name {
			// node groups that are not managed by the given set are retained as-is
			allNodeGroups = append(allNodeGroups, ng)
		} else {
			// node groups that are owned by given set are collected for updated or delete
			existing[ng.Name] = ng
		}
	}
	// add NodeGroups of the Set
	for i := range ngSet.NodeGroups {
		ng := nodegroup.ToGardenerNodeSet(ngSet.NodeGroups[i])
		// ensure the Set owner label
		if ng.Labels == nil {
			ng.Labels = map[string]string{}
		}
		ng.Labels[NodeGroupSetOwnerKey] = ngSet.Name
		if oldOne, ok := existing[ng.Name]; ok {
			delete(existing, ng.Name)
			updated, err := updateNodeGroup(oldOne, ng, policy)
			if err != nil {
				return nil, err
			}
			allNodeGroups = append(allNodeGroups, *updated)
			// maintain change list
			if !reflect.DeepEqual(&oldOne, updated) {
				changes = append(changes, NodeGroupChange{Operation: NodeGroupOpUpdate, Before: &oldOne, After: updated})
			}
		} else {
			allNodeGroups = append(allNodeGroups, ng)
			// maintain change list
			changes = append(changes, NodeGroupChange{Operation: NodeGroupOpCreate, After: &ng})
		}
	}
	// the left existing members do not appear in the new desired state, thus they will be deleted
	for _, v := range existing {
		worker := v // Fix exportloopref linter error, see https://stackoverflow.com/a/48826629
		changes = append(changes, NodeGroupChange{Operation: NodeGroupOpDelete, Before: &worker})
	}
	// order by name to keep the list stable
	sort.SliceStable(allNodeGroups, func(i, j int) bool {
		return allNodeGroups[i].Name < allNodeGroups[j].Name
	})
	shoot.Spec.Provider.Workers = allNodeGroups
	if policy == NodeGroupPolicyDryrun {
		return changes, nil
	}
	return changes, g.client.Update(ctx, shoot)
}

// updateNodeGroup compute the final node group spec according to the current spec, desired spec and the update policy
func updateNodeGroup(current gardencorev1beta1.Worker, desired gardencorev1beta1.Worker, policy NodeGroupPolicy) (*gardencorev1beta1.Worker, error) {
	var result *gardencorev1beta1.Worker
	switch policy {
	// dryrun all changes
	case NodeGroupPolicyRollingUpdate, NodeGroupPolicyDryrun:
		result = &desired
		// we should retain some auto-populated fields
		if result.Machine.Image != nil && result.Machine.Image.Version == nil {
			result.Machine.Image.Version = current.Machine.Image.Version
		}
		if result.MaxSurge == nil {
			result.MaxSurge = current.MaxSurge
		}
		if result.MaxUnavailable == nil {
			result.MaxUnavailable = current.MaxUnavailable
		}
		return result, nil
	case NodeGroupPolicyScaleOnly:
		// keep the spec same as current
		result = &current
		// scale to the desired replicas
		result.Minimum = desired.Minimum
		result.Maximum = desired.Maximum
		return result, nil
	default:
		return nil, fmt.Errorf("unknown policy %s", string(policy))
	}
}

func (g *GardEnvironment) DeleteShoot(ctx context.Context, name string) (alreadyDeleted bool, err error) {
	shoot := new(gardencorev1beta1.Shoot)
	err = g.client.Get(ctx, types.NamespacedName{Namespace: g.GetShootNamespace(), Name: name}, shoot)
	if err != nil {
		return
	}
	annotations := make(map[string]string)
	annotations["confirmation.garden.sapcloud.io/deletion"] = strTrue
	shoot.Annotations = annotations

	err = g.client.Update(ctx, shoot)
	if err != nil {
		return
	}

	if shoot.DeletionTimestamp != nil {
		alreadyDeleted = true
		return
	}

	err = g.client.Delete(ctx, shoot)
	if err != nil {
		return
	}

	return
}

func (g *GardEnvironment) buildShoot(
	shootName string,
	providerType string,
	region string,
	zones []string,
	workers []gardencorev1beta1.Worker,
	labels map[string]string,
	cidr string,
	workerCidr []string,
	publicLBCidr []string,
	internalLBCidr []string,
) (*gardencorev1beta1.Shoot, error) {
	seedName := fmt.Sprintf("%s-%s", providerType, region)
	dnsDomain := fmt.Sprintf("%s.%s.shoot.%s", shootName, g.projectName, g.baseDomain)
	kubeproxyMode := gardencorev1beta1.ProxyModeIPTables
	podsCIDR := "100.96.0.0/11"
	servicesCIDR := "100.64.0.0/13"

	// ref https://github.com/tidbcloud/dbaas/blob/80d6c65c64868fa85eb6648fd9a493d10f9a727f/central/runtime/deployer/gardener/shoot_cluster.go#L961
	// aws is the cidr
	// gcp is the worker part after splitting cidr
	var nodesCIDR string
	switch providerType {
	case consts.ProviderTypeAWS:
		nodesCIDR = cidr
	case consts.ProviderTypeGCP:
		nodesCIDR = workerCidr[0]
	}
	resourceTags := cloudv1.TagsFromLabels(labels)

	cloudProvider, err := getCloudProvider(providerType, zones, g.config, cidr, workerCidr, publicLBCidr, internalLBCidr)
	if err != nil {
		return nil, err
	}
	cloudProvider.Workers = workers

	var cidrMaskSize int32 = 24
	enableBasicAuth := false
	allowPrivilegedContainers := true
	return &gardencorev1beta1.Shoot{
		ObjectMeta: metav1.ObjectMeta{
			Name:      shootName,
			Namespace: g.GetShootNamespace(),
			Labels:    resourceTags,
		},

		Spec: gardencorev1beta1.ShootSpec{
			CloudProfileName: providerType,
			DNS: &gardencorev1beta1.DNS{
				Domain: &dnsDomain,
			},
			Kubernetes: gardencorev1beta1.Kubernetes{
				Version:                   getK8SVersion(providerType),
				AllowPrivilegedContainers: &allowPrivilegedContainers,
				KubeAPIServer: &gardencorev1beta1.KubeAPIServerConfig{
					EnableBasicAuthentication: &enableBasicAuth,
				},
				KubeControllerManager: &gardencorev1beta1.KubeControllerManagerConfig{
					NodeCIDRMaskSize: &cidrMaskSize,
				},
				KubeProxy: &gardencorev1beta1.KubeProxyConfig{
					Mode: &kubeproxyMode,
				},
			},
			Networking: gardencorev1beta1.Networking{
				Type:     "calico",
				Services: &servicesCIDR,
				Pods:     &podsCIDR,
				Nodes:    &nodesCIDR,
			},
			Provider: cloudProvider,
			Maintenance: &gardencorev1beta1.Maintenance{
				AutoUpdate: &gardencorev1beta1.MaintenanceAutoUpdate{
					KubernetesVersion:   false,
					MachineImageVersion: false,
				},
				TimeWindow: &gardencorev1beta1.MaintenanceTimeWindow{
					Begin: "190000+0000",
					End:   "200000+0000",
				},
			},
			Region:            region,
			SecretBindingName: providerType,
			SeedName:          &seedName,
			Hibernation:       nil,
		},
	}, nil
}

func getK8SVersion(providerType string) string {
	// It is urgent task to use gp3 type before PP. To reduce the risk, only aws use k8s 1.18
	if providerType == consts.ProviderTypeAWS {
		return "1.18.17"
	}

	return "1.16"
}

func getCloudProvider(
	providerType string,
	zones []string,
	config *cloudv1.Gardener,
	cidr string,
	workerCidr []string,
	publicLBCidr []string,
	internalLBCidr []string,
) (gardencorev1beta1.Provider, error) {
	switch providerType {
	case consts.ProviderTypeAWS:
		controlPlaneConfig := awsv1alpha1.ControlPlaneConfig{
			TypeMeta: metav1.TypeMeta{
				Kind:       "ControlPlaneConfig",
				APIVersion: awsv1alpha1.SchemeGroupVersion.String(),
			},
		}

		// The marshal should always succeed
		controlPlaneConfigData, _ := json.Marshal(controlPlaneConfig)

		enableECRAccess := true
		publicSubnets := publicLBCidr
		privateSubnets := internalLBCidr
		subnets := workerCidr

		if len(zones) != len(publicSubnets) || len(zones) != len(privateSubnets) || len(zones) != len(subnets) {
			return gardencorev1beta1.Provider{}, fmt.Errorf("zone size exceed 3")
		}
		// privateSubnets := [3]string{"10.250.48.0/20", "10.250.112.0/20", "10.250.176.0/20"}

		awsZones := make([]awsv1alpha1.Zone, len(zones))
		for i, z := range zones {
			awsZones[i] = awsv1alpha1.Zone{
				Name:     z,
				Workers:  subnets[i],
				Public:   publicSubnets[i],
				Internal: privateSubnets[i],
			}
		}

		infraConfig := awsv1alpha1.InfrastructureConfig{
			TypeMeta: metav1.TypeMeta{
				Kind:       "InfrastructureConfig",
				APIVersion: awsv1alpha1.SchemeGroupVersion.String(),
			},
			EnableECRAccess: &enableECRAccess,
			Networks: awsv1alpha1.Networks{
				VPC: awsv1alpha1.VPC{
					CIDR: &cidr,
				},
				Zones: awsZones,
			},
		}

		// The marshal should always succeed
		infraConfigData, _ := json.Marshal(infraConfig)

		return gardencorev1beta1.Provider{
			Type: consts.ProviderTypeAWS,
			ControlPlaneConfig: &gardencorev1beta1.ProviderConfig{
				RawExtension: runtime.RawExtension{
					Raw: controlPlaneConfigData,
				},
			},
			InfrastructureConfig: &gardencorev1beta1.ProviderConfig{
				RawExtension: runtime.RawExtension{
					Raw: infraConfigData,
				},
			},
		}, nil

	case consts.ProviderTypeGCP:
		if len(internalLBCidr) != 1 {
			return gardencorev1beta1.Provider{},
				errors.Errorf("gcp should have only one internalLB cidr, but get %v", internalLBCidr)
		}

		controlPlaneConfig := gcpv1alpha1.ControlPlaneConfig{
			TypeMeta: metav1.TypeMeta{
				Kind:       "ControlPlaneConfig",
				APIVersion: gcpv1alpha1.SchemeGroupVersion.String(),
			},
		}
		// The marshal should always succeed
		controlPlaneConfigData, _ := json.Marshal(controlPlaneConfig)
		infraConfig := gcpv1alpha1.InfrastructureConfig{
			TypeMeta: metav1.TypeMeta{
				Kind:       "InfrastructureConfig",
				APIVersion: gcpv1alpha1.SchemeGroupVersion.String(),
			},
			Networks: gcpv1alpha1.NetworkConfig{
				Workers:  workerCidr[0],
				Internal: &internalLBCidr[0],
			},
		}
		// The marshal should always succeed
		infraConfigData, _ := json.Marshal(infraConfig)

		return gardencorev1beta1.Provider{
			Type: consts.ProviderTypeGCP,
			ControlPlaneConfig: &gardencorev1beta1.ProviderConfig{
				RawExtension: runtime.RawExtension{
					Raw: controlPlaneConfigData,
				},
			},
			InfrastructureConfig: &gardencorev1beta1.ProviderConfig{
				RawExtension: runtime.RawExtension{
					Raw: infraConfigData,
				},
			},
		}, nil
	default:
		return gardencorev1beta1.Provider{}, errors.Errorf("Unknown provider type %s", providerType)
	}
}

// ShootNamespaceInSeed generate the namespace of shoot control plane in the seed cluster
func ShootNamespaceInSeed(project, shootName string) string {
	return fmt.Sprintf("shoot--%s--%s", project, shootName)
}

// CancelShootContext stop list-watch in controller runtime manager
func (g *GardEnvironment) CancelShootContext(name string) {
	g.shootMu.Lock()
	defer g.shootMu.Unlock()

	if sctx, ok := g.shoots[name]; ok {
		sctx.cancel()
		delete(g.shoots, name)
	}
}

// AwakeShoot update shoot hibernation to false and set awake annotations
func (g *GardEnvironment) AwakeShoot(ctx context.Context, name string) error {
	shoot, err := g.GetShoot(ctx, name)
	if err != nil {
		return err
	}
	if EnableHibernation(shoot) {
		newShoot := shoot.DeepCopy()
		if newShoot.Annotations == nil {
			newShoot.Annotations = make(map[string]string)
		}
		// set awake time to shoot annotations
		newShoot.Annotations[AwakeTimeAnnotation] = time.Now().Format(time.RFC3339)

		// set shoot hibernation to false
		newShoot.Spec.Hibernation = &gardencorev1beta1.Hibernation{
			Enabled: pointer.BoolPtr(false),
		}
		if err := g.client.Update(ctx, newShoot); err != nil {
			return err
		}
	}
	return nil
}

// HibernateShoot update shoot hibernation to true
func (g *GardEnvironment) HibernateShoot(ctx context.Context, name string) error {
	shoot, err := g.GetShoot(ctx, name)
	if err != nil {
		return err
	}
	if !EnableHibernation(shoot) {
		newShoot := shoot.DeepCopy()
		newShoot.Spec.Hibernation = &gardencorev1beta1.Hibernation{
			Enabled: pointer.BoolPtr(true),
		}
		if err := g.client.Update(ctx, newShoot); err != nil {
			return err
		}
	}
	return nil
}

// EnableHibernation return true when shoot hibernation is true
func EnableHibernation(s *gardencorev1beta1.Shoot) bool {
	return s.Spec.Hibernation != nil && s.Spec.Hibernation.Enabled != nil && *s.Spec.Hibernation.Enabled
}

func IsWakingUp(s *gardencorev1beta1.Shoot) bool {
	return !EnableHibernation(s) && s.Status.IsHibernated
}

func (g *GardEnvironment) Started() bool {
	return g.started
}
