package cluster

import (
	"context"
	"fmt"

	"github.com/pingcap/log"
	regionalpb "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/constants"
	bizerr "github.com/tidbcloud/infra-provider/regional-server/pkg/common/errors"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/rules"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	rsconfig "github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/account"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appbox"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appcontroller"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/approute"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/internet"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/namespace"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/vpc"
	svcaccount "github.com/tidbcloud/infra-provider/regional-server/pkg/service/account"
	"go.uber.org/zap"
	"google.golang.org/protobuf/encoding/protojson"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

type ClusterService struct {
	vpcrw           vpc.VPCReaderWriter
	internetrw      internet.InternetReaderWriter
	appboxrw        appbox.APPBoxReaderWriter
	appcontrollerrw appcontroller.APPControllerReaderWriter
	approuterw      approute.APPRouteReaderWriter
	namespacerw     namespace.NamespaceReaderWriter
	appboxrule      rules.AppboxRule
	config          rsconfig.RootConfig
	tmplconfig      rsconfig.TemplatesConfig
	accountsvc      svcaccount.ServiceInterface
	sharedbucketsvc svcaccount.SharedBucketServiceInterface
	accountReader   account.AccountReader
}

func NewClusterService(
	vpcrw vpc.VPCReaderWriter,
	internetrw internet.InternetReaderWriter,
	appboxrw appbox.APPBoxReaderWriter,
	appcontrollerrw appcontroller.APPControllerReaderWriter,
	approuterw approute.APPRouteReaderWriter,
	namespacerw namespace.NamespaceReaderWriter,
	accountsvc svcaccount.ServiceInterface,
	sharedbucketsvc svcaccount.SharedBucketServiceInterface,
	accountReader account.AccountReader,
	config rsconfig.RootConfig,
	tmplconfig rsconfig.TemplatesConfig,
) *ClusterService {
	return &ClusterService{
		vpcrw:           vpcrw,
		internetrw:      internetrw,
		appboxrw:        appboxrw,
		appcontrollerrw: appcontrollerrw,
		approuterw:      approuterw,
		namespacerw:     namespacerw,
		appboxrule:      &rules.AppboxDefaultRule{},
		config:          config,
		tmplconfig:      tmplconfig,
		accountsvc:      accountsvc,
		sharedbucketsvc: sharedbucketsvc,
		accountReader:   accountReader,
	}
}

func (c *ClusterService) Create(ctx context.Context, req *regionalpb.CreateClusterRequest) (regionalpb.Cluster, error) {
	log.Info("create cluster", zap.Any("req", req))
	namer := utils.NewNamer(req.Cluster.TenantId, req.Cluster.ProjectId, req.Cluster.Id, c.config.RegionConfig.Name, c.config.Env)
	// make sure namespace created
	if err := c.ensureNamespace(ctx, namer); err != nil {
		return regionalpb.Cluster{}, err
	}
	// if approute exists, return 409
	var approute v1alpha1.APPRoute
	err := c.approuterw.Get(ctx, client.ObjectKey{
		Namespace: namer.GetNamespace(),
		Name:      namer.GetAPPRouteName(),
	}, &approute)
	if err == nil {
		return regionalpb.Cluster{
			Id: req.Cluster.Id,
		}, bizerr.New(bizerr.ResourceConflict, fmt.Sprintf("cluster already exist with status %s", string(approute.Status.Phase)))
	}
	if client.IgnoreNotFound(err) != nil {
		return regionalpb.Cluster{}, err
	}
	// make sure vpc created
	vpcname, accountRef, err := c.ensureVPC(ctx, namer, req)
	if err != nil {
		return regionalpb.Cluster{}, err
	}
	//make sure internet created
	_, err = c.ensureInternet(ctx, namer, vpcname, req)
	if err != nil {
		return regionalpb.Cluster{}, err
	}
	// make sure appbox created
	appboxname, err := c.ensureAPPBox(ctx, namer, vpcname, req)
	if err != nil {
		return regionalpb.Cluster{}, err
	}
	// assign shared bucket
	bucketRef, err := c.assignSharedBucket(ctx, accountRef, req)
	if err != nil {
		return regionalpb.Cluster{}, err
	}
	//create approute
	err = c.createAPPRoute(ctx, namer, vpcname, appboxname, bucketRef, req)
	if err != nil {
		return regionalpb.Cluster{}, err
	}

	return regionalpb.Cluster{
		Id: req.Cluster.Id,
	}, nil
}

func (c *ClusterService) ensureNamespace(ctx context.Context, namer *utils.Namer) error {
	var ns corev1.Namespace
	err := c.namespacerw.Get(ctx, client.ObjectKey{Name: namer.GetNamespace()}, &ns)
	if err == nil {
		return nil
	}
	// If the namespace does not exist, create it.
	if client.IgnoreNotFound(err) != nil {
		return err
	}

	err = c.namespacerw.Create(ctx, &corev1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name: namer.GetNamespace(),
		},
	})
	// If the namespace already exists, do nothing
	if err != nil && apierrors.IsAlreadyExists(err) {
		return nil
	}

	return err
}

func (c *ClusterService) ensureVPC(ctx context.Context, namer *utils.Namer, req *regionalpb.CreateClusterRequest) (string, *v1alpha1.AccountReference, error) {
	//if vpc id set, check vpc exits and status
	if req.Cluster.VpcSettings.GetExistingAwsVpcId() != "" ||
		req.Cluster.VpcSettings.GetExistingGcpNetworkName() != "" ||
		req.Cluster.VpcSettings.GetExistingAzureVnetId() != "" {
		var vpclist v1alpha1.VPCList
		var vpckeys string
		switch c.config.Provider {
		case string(constants.GCP), consts.ProviderTypeGKE:
			vpckeys = req.Cluster.VpcSettings.GetExistingGcpNetworkName()
		case string(constants.AWS), consts.ProviderTypeEKS:
			vpckeys = req.Cluster.VpcSettings.GetExistingAwsVpcId()
		case string(constants.Azure), consts.ProviderTypeAKS:
			vpckeys = req.Cluster.VpcSettings.GetExistingAzureVnetId()
		}
		err := c.vpcrw.List(ctx, &vpclist, client.InNamespace(namer.GetNamespace()), client.MatchingLabels{
			v1alpha1.VPCIDKey: vpckeys,
		})
		if err != nil {
			return "", nil, err
		}
		//in normal case, vpc id is unique, so len(vpclist.Items) must be 1
		if len(vpclist.Items) != 1 {
			return "", nil, bizerr.New(bizerr.ParameterInvalid, "vpc not exist")
		}
		vpc := vpclist.Items[0]

		// check vpc is hiberated or not and remove the hibernated anno
		isHibernated := vpc.Annotations[v1alpha1.AnnotationKeyIsHiberated] == "true"
		if isHibernated {
			delete(vpc.Annotations, v1alpha1.AnnotationKeyIsHiberated)
			if err := c.vpcrw.Update(ctx, &vpc); err != nil {
				return "", nil, bizerr.New(bizerr.K8SClientError, err.Error())
			}
		}
		//check vpc status
		switch vpc.Status.Phase {
		//if ready to delete , modify status to ready
		case v1alpha1.VPCPhaseReadyForDelete:
			{
				vpc.Status.Phase = v1alpha1.VPCPhaseActive
				if err := c.vpcrw.UpdateStatus(ctx, &vpc); err != nil {
					return "", nil, bizerr.New(bizerr.K8SClientError, err.Error())
				}
				return vpc.Name, vpc.Spec.AccountRef, nil
			}
		// if terminating , return err
		case v1alpha1.VPCPhaseFailed, v1alpha1.VPCPhaseDeleted, v1alpha1.VPCPhaseDeleting:
			{
				return "", nil, bizerr.New(bizerr.ParameterInvalid, "vpc status is terminating or not ready, can not create cluster")
			}
		}
		return vpc.Name, vpc.Spec.AccountRef, nil
	}

	// if vpc id not set, check cidr is seted, just create a new vpc
	if req.Cluster.VpcSettings.GetNewCidrBlock() != "" {
		// use generate a unique name by project id and cluster id, in case concurrent request with same cluster id
		name := namer.GetUniqueName()
		accountInfo, providerConfigName, err := c.accountsvc.AssignAccount(ctx, name, req.Cluster.TenantId, req.Cluster.ProjectId)
		if err != nil {
			return "", nil, err
		}

		accountRef := accountInfo.GetAccountReference(providerConfigName)
		if err := c.createVPC(ctx, name, req.Cluster.VpcSettings.GetNewCidrBlock(), req, namer, accountRef); err != nil {
			return "", nil, err
		}
		return name, accountRef, nil
	}

	return "", nil, bizerr.New(bizerr.ParameterInvalid, "vpc id or cidr is required")
}

func (c *ClusterService) createVPC(ctx context.Context, name string, cidr string, req *regionalpb.CreateClusterRequest, namer *utils.Namer, account *v1alpha1.AccountReference) error {
	vpc := v1alpha1.VPC{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: utils.NewNamerWithClusterID(req.Cluster.Id, c.config.RegionConfig.Name, c.config.Env).GetNamespace(),
			//add labels project，tennetid，region
			Labels: map[string]string{
				v1alpha1.RegionKey:         c.config.RegionConfig.Name,
				namer.GetProjectLabelKey(): "true",
				namer.GetTenantLabelKey():  "true",
				v1alpha1.AccountIDKey:      account.AccountID,
				// add tags for billing
				v1alpha1.TagProjectKey: namer.GetProjectID(),
				v1alpha1.TagTenantKey:  namer.GetTenantID(),
			},
		},
		Spec: v1alpha1.VPCSpec{
			Provider:   c.config.Provider,
			Region:     c.config.RegionConfig.Name,
			CIDR:       cidr,
			AccountRef: account,
		},
	}
	vpc.ObjectMeta.Labels = util.MergeMaps(vpc.ObjectMeta.Labels, c.config.ResourceLabels)
	if err := c.vpcrw.Create(ctx, &vpc); err != nil {
		if apierrors.IsAlreadyExists(err) {
			log.Info("vpc already exists", zap.String("name", name))
			return nil
		}
		return err
	}
	return nil
}

func (c *ClusterService) ensureInternet(ctx context.Context, namer *utils.Namer, vpcname string, req *regionalpb.CreateClusterRequest) (string, error) {
	var internetlist v1alpha1.InternetList
	err := c.internetrw.List(ctx, &internetlist, &client.MatchingLabels{
		v1alpha1.VPCNameKey: vpcname,
	}, client.InNamespace(namer.GetNamespace()))
	if err != nil {
		return "", err
	}
	if len(internetlist.Items) > 0 {
		//choose most recent internet,internet name like project_id-1
		internet := &internetlist.Items[0]
		for i, inner := range internetlist.Items {
			if utils.NameLargerThan(inner.Name, internet.Name) {
				internet = &internetlist.Items[i]
			}
		}
		// check internet is hiberated or not?
		isHibernated := internet.Annotations[v1alpha1.AnnotationKeyIsHiberated] == "true"
		if isHibernated {
			delete(internet.Annotations, v1alpha1.AnnotationKeyIsHiberated)
			if err := c.internetrw.Update(ctx, internet); err != nil {
				return "", err
			}
		}

		//check internet status
		switch internet.Status.Phase {
		//if ready to delete , modify status to ready
		case v1alpha1.InternetPhaseReadyForDelete:
			{
				internet.Status.Phase = v1alpha1.InternetPhaseActive
				if err := c.internetrw.UpdateStatus(ctx, internet); err != nil {
					return "", err
				}
				return internet.Name, nil
			}
		//if terminating , create a new internet
		case v1alpha1.InternetPhaseFailed, v1alpha1.InternetPhaseDeleted, v1alpha1.InternetPhaseDeleting:
			{
				if err := c.createInternet(ctx, utils.InceareName(internet.Name), vpcname, namer, req); err != nil {
					return "", err
				}
				return utils.InceareName(internet.Name), nil
			}
		}
		return internet.Name, nil
	}
	//if internet not exits, create a new internet
	name := fmt.Sprintf("%s-%d", vpcname, 0)
	if err := c.createInternet(ctx, name, vpcname, namer, req); err != nil {
		return "", err
	}

	return name, nil
}

func (c *ClusterService) createInternet(ctx context.Context, name, vpcname string, namer *utils.Namer, req *regionalpb.CreateClusterRequest) error {
	internet := v1alpha1.Internet{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namer.GetNamespace(),
			//add labels project，tennetid，region
			Labels: map[string]string{
				v1alpha1.RegionKey:         c.config.RegionConfig.Name,
				namer.GetProjectLabelKey(): "true",
				namer.GetTenantLabelKey():  "true",
				v1alpha1.VPCNameKey:        vpcname,
				// add tags for billing
				v1alpha1.TagProjectKey: namer.GetProjectID(),
				v1alpha1.TagTenantKey:  namer.GetTenantID(),
			},
		},
		Spec: v1alpha1.InternetSpec{
			Provider: c.config.Provider,
			Region:   c.config.RegionConfig.Name,
			VPCRef: &v1alpha1.Reference{
				Name:      vpcname,
				Namespace: namer.GetNamespace(),
			},
		},
	}
	internet.ObjectMeta.Labels = util.MergeMaps(internet.ObjectMeta.Labels, c.config.ResourceLabels)
	if err := c.internetrw.Create(ctx, &internet); err != nil {
		if apierrors.IsAlreadyExists(err) {
			log.Info("internet already exists", zap.String("name", name))
			return nil
		}
		return err
	}
	return nil
}

func (c *ClusterService) ensureAPPBox(ctx context.Context, namer *utils.Namer, vpcname string, req *regionalpb.CreateClusterRequest) (string, error) {
	var appboxlist v1alpha1.APPBoxList
	err := c.appboxrw.List(ctx, &appboxlist, client.InNamespace(namer.GetNamespace()), client.MatchingLabels{
		v1alpha1.VPCNameKey: vpcname,
	})
	if err != nil {
		return "", err
	}

	if len(appboxlist.Items) > 0 {
		//choose appbox by rules ,appbox name like vpcname-0, vpcname-1
		appbox := c.appboxrule.ChooseAppBox(&appboxlist)
		//check appbox status
		switch appbox.Status.Phase {
		//if ready to delete , modify status to ready
		case v1alpha1.APPBoxPhaseReadyForDelete:
			{
				appbox.Status.Phase = v1alpha1.APPBoxPhaseActive
				if err := c.appboxrw.UpdateStatus(ctx, appbox); err != nil {
					return "", err
				}
				return appbox.Name, nil
			}
		//if terminating , create a new appbox
		case v1alpha1.APPBoxPhaseFailed, v1alpha1.APPBoxPhaseDeleting:
			{
				if err := c.createAPPBox(ctx, utils.InceareName(appbox.Name), vpcname, namer, req); err != nil {
					return "", err
				}
				return utils.InceareName(appbox.Name), nil
			}
		}
		return appbox.Name, nil
	}
	//if appbox not exits, create a new appbox
	name := fmt.Sprintf("%s-%d", vpcname, 0)
	if err := c.createAPPBox(ctx, name, vpcname, namer, req); err != nil {
		return "", err
	}

	return name, nil
}

func (c *ClusterService) createAPPBox(ctx context.Context, name string, vpcname string, namer *utils.Namer, _ *regionalpb.CreateClusterRequest) error {
	version := c.config.APPBoxConfig.GetK8sVersion(namer.GetTenantID(), namer.GetProjectID())
	appbox := v1alpha1.APPBox{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namer.GetNamespace(),
			//add labels project，tennetid，region
			Labels: map[string]string{
				v1alpha1.RegionKey:         c.config.RegionConfig.Name,
				v1alpha1.VPCNameKey:        vpcname,
				v1alpha1.ManageKey:         v1alpha1.ManageFull,
				namer.GetProjectLabelKey(): "true",
				namer.GetTenantLabelKey():  "true",
				// add tags for billing
				v1alpha1.TagProjectKey: namer.GetProjectID(),
				v1alpha1.TagTenantKey:  namer.GetTenantID(),
			},
		},
		Spec: v1alpha1.APPBoxSpec{
			Provider: c.config.Provider,
			Region:   c.config.RegionConfig.Name,
			KubernetesConfig: v1alpha1.KubernetesConfig{
				Version:             version,
				AddonTemplateID:     c.tmplconfig.GetDefaultAddonTemplateId(),
				InfraTemplateID:     c.tmplconfig.GetDefaultInfraTemplateIdWithVersion(c.config.APPBoxConfig.K8sVersion),
				DedicatedTemplateID: c.tmplconfig.GetDefaultDedicatedTemplateIdWithVersion(c.config.APPBoxConfig.K8sVersion),
				DPTemplateID:        c.tmplconfig.GetDefaultDPTemplateIdWithVersion(c.config.APPBoxConfig.K8sVersion),
			},
			VPCRef: &v1alpha1.Reference{
				Name:      vpcname,
				Namespace: namer.GetNamespace(),
			},
		},
	}
	appbox.ObjectMeta.Labels = util.MergeMaps(appbox.ObjectMeta.Labels, c.config.ResourceLabels)
	if err := c.appboxrw.Create(ctx, &appbox); err != nil {
		if apierrors.IsAlreadyExists(err) {
			log.Info("appbox already exists", zap.String("name", name))
			return nil
		}
		return err
	}
	return nil
}

func (c *ClusterService) assignSharedBucket(ctx context.Context, accountRef *v1alpha1.AccountReference, req *regionalpb.CreateClusterRequest) (*v1alpha1.SharedBucketReference, error) {
	if !config.FeatureEnabled(c.config.FeatureGates, config.AllocateSharedBucket, req.Cluster.TenantId, req.Cluster.ProjectId) {
		log.Info("skip allocate shared bucket as feature gate is not enabled", zap.String("tenant_id", req.Cluster.TenantId), zap.String("project_id", req.Cluster.ProjectId))
		return nil, nil
	}

	account, err := c.accountReader.GetByRef(ctx, accountRef)
	if err != nil {
		return nil, fmt.Errorf("get account: %w", err)
	}
	bucket, err := c.sharedbucketsvc.AssignSharedBucket(ctx, account, req.Cluster.Id, req.Cluster.TenantId, req.Cluster.ProjectId)
	if err != nil {
		return nil, err
	}
	return bucket.GetSharedBucketReference(account, accountRef.ProviderConfigName), nil
}

func (c *ClusterService) createAPPRoute(ctx context.Context, namer *utils.Namer, vpcname string, appboxname string, bucketRef *v1alpha1.SharedBucketReference, req *regionalpb.CreateClusterRequest) error {
	approute := v1alpha1.APPRoute{
		ObjectMeta: metav1.ObjectMeta{
			Name:      namer.GetAPPRouteName(),
			Namespace: namer.GetNamespace(),
			//add labels project，tennetid，region
			Labels: map[string]string{
				v1alpha1.RegionKey:         c.config.RegionConfig.Name,
				v1alpha1.VPCNameKey:        vpcname,
				v1alpha1.APPBoxNameKey:     appboxname,
				v1alpha1.ClusterIDKey:      namer.GetClusterID(),
				namer.GetProjectLabelKey(): "true",
				namer.GetTenantLabelKey():  "true",
				namer.GetClusterLabelKey(): "true",
				// add tags for billing
				v1alpha1.TagClusterKey: namer.GetClusterID(),
				v1alpha1.TagProjectKey: namer.GetProjectID(),
				v1alpha1.TagTenantKey:  namer.GetTenantID(),
			},
		},
		Spec: v1alpha1.APPRouteSpec{
			VPCRef: &v1alpha1.Reference{
				Name:      vpcname,
				Namespace: namer.GetNamespace(),
			},
			APPBoxRef: &v1alpha1.Reference{
				Name:      appboxname,
				Namespace: namer.GetNamespace(),
			},
			SharedBucketRef: bucketRef,
		},
	}
	approute.ObjectMeta.Labels = util.MergeMaps(approute.ObjectMeta.Labels, c.config.ResourceLabels)
	bs, err := protojson.Marshal(req.Cluster)
	if err != nil {
		return err
	}
	approute.Spec.ForCluster = string(bs)
	if err := c.approuterw.Create(ctx, &approute); err != nil {
		if apierrors.IsAlreadyExists(err) {
			log.Info("approute already exists", zap.String("name", namer.GetAPPRouteName()))
			return nil
		}
		return err
	}
	return nil
}

func (c *ClusterService) Delete(ctx context.Context, req *regionalpb.DeleteClusterRequest) (regionalpb.Cluster, error) {
	if err := c.sharedbucketsvc.UnassignSharedBucket(ctx, req.ClusterId); err != nil {
		return regionalpb.Cluster{}, err
	}

	//check approute exits,if not exits return success
	namer := utils.NewNamerWithClusterID(req.ClusterId, c.config.RegionConfig.Name, c.config.Env)
	approute := &v1alpha1.APPRoute{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: namer.GetNamespace(),
			Name:      namer.GetAPPRouteName(),
		},
	}
	//delete approute
	if err := c.approuterw.Delete(ctx, approute); err != nil {
		if apierrors.IsNotFound(err) {
			log.Info("approute not found", zap.String("name", namer.GetAPPRouteName()))
			return regionalpb.Cluster{}, bizerr.New(bizerr.ResourceNotFound, "cluster not found")
		}
		log.Error("failed to delete approute", zap.Error(err))
		return regionalpb.Cluster{}, err
	}
	return regionalpb.Cluster{}, nil
}
