package cluster

import (
	"context"
	"strconv"
	"time"

	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/sdk/auditlog"

	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/v1beta1"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/entity"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/apimiddleware/bizerror"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/pkg/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (c serverImpl) CreateCluster(
	ctx context.Context,
	req *openapiserverlessv1beta1.CreateClusterRequest,
) (respCluster *openapiserverlessv1beta1.Cluster, err error) {
	var (
		reqMeta   *metadata.RequestMeta
		clusterID uint64
		tags      []string
	)
	tenantID := unknownTenantID
	startTime := time.Now()
	defer func() {
		recordWriteClusterAPIMetrics(apiCreateCluster, tenantID, time.Since(startTime).Seconds(), err)
	}()

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeServerless)
		reqMeta.ClusterID = clusterID
		details = eventbus.AppendServerlessClusterDetails(details, req.Cluster)
		return details
	}
	defer func() {
		c.auditLogSender.SubmitAuditLog(ctx, auditlog.EventCreateCluster, reqMeta, detailsFunc, err)
	}()

	if req.Cluster == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster object should not be nil")
	}

	tags, err = normalizeOpenAPITags(req.GetCluster().GetTags())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid tags")
	}

	// Extract request meta.
	reqMeta, err = metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}
	tenantID = strconv.FormatUint(reqMeta.OrgID, 10)

	if req.Cluster.GetSpendingLimit().GetMonthly() < 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "spending limit should be greater than or equal to 0")
	} else if req.Cluster.GetSpendingLimit().GetMonthly() > int32(1000000) {
		return nil, bizerr.New(
			bizerr.ErrCodeParams,
			"Spending limit cannot exceed $10000. If you require a higher limit, please contact our sales team for assistance.",
		)
	}

	// Get project id from cluster labels. If not found, use the default project id.
	projectId := reqMeta.DefaultProjectID

	// use project id from request label if exists
	if req.GetCluster().Labels != nil {
		projectIdStr, ok := req.Cluster.Labels[common.ProjectIDLabelKey]
		if ok {
			projectId, err = strconv.ParseUint(projectIdStr, 10, 64)
			if err != nil {
				return nil, bizerr.WrapErrorWithCode(
					err,
					bizerr.ErrCodeParams,
					"invalid %s label",
					common.ProjectIDLabelKey,
				)
			}
		}
	}

	// Parse the region field.
	if req.GetCluster().Region == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region is invalid")
	}
	provider, region, err := common.ParseOpenApiRegionNameOrId(req.GetCluster().GetRegion().GetName())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region, please check and try again.")
	}

	// When CheckXProjectType is enabled, verify that the target project is of
	// type x-project (PROJECT_TYPE_TIDBX) or virtual-x-project
	// (PROJECT_TYPE_TIDBXVIRTUAL). Starter/Essential clusters must not be
	// created in regular dedicated projects.
	if c.oneConsoleConfig != nil && c.oneConsoleConfig.CheckXProjectType && c.accountCli != nil {
		project, projErr := c.accountCli.GetProject(ctx, projectId)
		if projErr != nil {
			return nil, bizerr.WrapErrorWithCode(projErr, bizerr.ErrCodeParams, "failed to get project %d", projectId)
		}
		if project.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBXVIRTUAL &&
			project.GetType() != accountsdk.ProjectType_PROJECT_TYPE_TIDBX {
			return nil, bizerr.New(bizerr.ErrCodeParams, "project %d is not a valid x-project or virtual-x-project type", projectId)
		}
	}

	// Check the cluster name format
	if !common.CheckDisplayNameFmt(req.GetCluster().GetDisplayName()) {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster name not allowed: %s", req.GetCluster().GetDisplayName())
	}

	cluster, err := buildCreateCluster(projectId, provider, region, reqMeta, req.GetCluster())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "request for create cluster is invalid")
	}

	// Then create the cluster in domain service
	cluster, err = c.clusterSvc.CreateCluster(ctx, cluster)
	if err != nil {
		return nil, err
	}
	clusterID = cluster.ClusterId

	if len(tags) > 0 {
		clusterTags, bindErr := c.clusterSvc.BindTags(ctx, reqMeta.OrgID, cluster.ClusterId, tags)
		if bindErr != nil {
			return nil, bizerror.NewInternal(bindErr, "failed to bind tags during cluster creation")
		}
		cluster.Tags = clusterTags
	}

	respCluster, err = buildClusterFromEntity(cluster, nil, openapiserverlessv1beta1.ClusterView_BASIC)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to get cluster")
	}

	return respCluster, nil
}

func buildCreateCluster(
	prjID uint64,
	provider, region string,
	reqMeta *metadata.RequestMeta,
	cluster *openapiserverlessv1beta1.Cluster,
) (*entity.Cluster, error) {
	cloudProvider, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](provider)
	if err != nil {
		return nil, err
	}

	createCluster := &entity.Cluster{
		ProjectId:      prjID,
		OrganizationId: reqMeta.OrgID,
		DisplayName:    cluster.DisplayName,
		UserId:         reqMeta.UserID,
		Labels:         cluster.Labels,
		Annotations:    cluster.Annotations,
		DeployType:     entity.DeployTypeServerless,
		CloudProvider:  openapicommonv1beta1.Region_CloudProvider(cloudProvider),
		Region:         region,
		CreatedBy:      common.BuildCreatedBy(reqMeta.Email, reqMeta.ApiPublicKey),
		ServicePlan:    openapicommonv1beta1.ServicePlan_Starter,
		EngineType:     openapicommonv1beta1.EngineType_MYSQL_COMPATIBLE,
	}

	if cluster.GetEngineType() != openapicommonv1beta1.EngineType_ENGINE_TYPE_UNSPECIFIED {
		createCluster.EngineType = cluster.GetEngineType()
	}

	serverlessCluster := &globalserverlessv1.ServerlessServiceCluster{
		Cluster:       &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{},
		TenantId:      reqMeta.OrgID,
		ProjectId:     prjID,
		CloudProvider: provider,
		Region:        region,
	}

	// set user id to labels
	if createCluster.Labels == nil {
		createCluster.Labels = make(map[string]string)
	}
	createCluster.Labels[metadata.UserIDHeaderKey] = reqMeta.UserID

	if cluster.SpendingLimit != nil {
		serverlessCluster.SpendLimit = &globalserverlessv1.SpendLimit{
			Monthly: uint32(cluster.SpendingLimit.Monthly),
		}
	}

	if cluster.Endpoints != nil && cluster.Endpoints.Public != nil {
		serverlessCluster.Cluster.Endpoints = &regionalserverlessv1.Endpoints{
			Public: &regionalserverlessv1.PublicEndpoint{
				Disabled: cluster.Endpoints.Public.Disabled,
			},
		}
	}

	// set advanced encryption config
	if cluster.EncryptionConfig != nil {
		serverlessCluster.Cluster.EnhancedEncryptionEnabled = cluster.EncryptionConfig.EnhancedEncryptionEnabled
	}

	// set root pwd
	serverlessCluster.RootPassword = cluster.RootPassword

	switch cluster.GetHighAvailabilityType() {
	case openapiserverlessv1beta1.Cluster_ZONAL:
		serverlessCluster.Cluster.ZoneRedundantEnabled = false
	case openapiserverlessv1beta1.Cluster_REGIONAL:
		serverlessCluster.Cluster.ZoneRedundantEnabled = true
	default:
		serverlessCluster.Cluster.ZoneRedundantEnabled = false
	}

	if cluster.AuditLogConfig != nil {
		serverlessCluster.Cluster.AuditLogConfig = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AuditLogConfig{
			Enabled:    cluster.AuditLogConfig.Enabled,
			Unredacted: cluster.AuditLogConfig.Unredacted,
		}
	}

	if cluster.AutomatedBackupPolicy != nil {
		policy := &globalserverlessv1.AutomatedBackupPolicy{
			BackupTime:    cluster.AutomatedBackupPolicy.StartTime,
			RetentionDays: uint64(cluster.AutomatedBackupPolicy.RetentionDays),
		}
		serverlessCluster.BackupPolicy = policy
	}

	if cluster.GetAutoScaling().GetMinRcu() > 0 || cluster.GetAutoScaling().GetMaxRcu() > 0 {
		/* essential plan */
		serverlessCluster.Cluster.AutoScaling = &regionalserverlessv1.ServerlessRegionalServiceClusterInfo_AutoScaling{
			MinRcu: cluster.GetAutoScaling().GetMinRcu(),
			MaxRcu: cluster.GetAutoScaling().GetMaxRcu(),
		}
		serverlessCluster.SpendLimit = nil
		createCluster.ServicePlan = openapicommonv1beta1.ServicePlan_Essential
	}

	createCluster.GlobalCluster = serverlessCluster

	return createCluster, nil
}
