package cluster

import (
	"context"
	"errors"
	"fmt"
	"strconv"

	accountsdk "github.com/tidbcloud/account/idl/pbgen/proto/account"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/v1beta1"
	openapidedicatedv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/dedicated/v1beta1"
	appcommon "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/app/common"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/adapter/eventbus"
	"github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/domain/constant"
	"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 *openapidedicatedv1beta1.CreateClusterRequest,
) (*openapidedicatedv1beta1.Cluster, error) {
	ctx, span := tracer.Start(ctx, "CreateCluster")
	defer span.End()
	var (
		err       error
		reqMeta   *metadata.RequestMeta
		clusterID uint64
		tags      []string
	)

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeDedicated)
		reqMeta.ClusterID = clusterID
		details = eventbus.AppendDedicatedClusterDetails(details, req.Cluster)

		if req.GetCluster().GetTidbNodeSetting() != nil &&
			len(req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()) > 0 {
			details[auditlog.KeyTidbSize] = req.GetCluster().GetTidbNodeSetting().GetNodeSpecKey()
			details[auditlog.KeyTidbQuantity] = req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()[0].GetNodeCount()
			details[auditlog.KeyTidbNodeGroupId] = req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()[0].GetTidbNodeGroupId()
			if req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()[0].GetTiproxySetting() != nil {
				details[auditlog.KeyTiProxySize] = req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()[0].GetTiproxySetting().
					GetNodeSpecKey()
				details[auditlog.KeyTiProxyQuantity] = req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()[0].GetTiproxySetting().
					GetNodeCount()
			}
		}

		if req.GetCluster().GetTikvNodeSetting() != nil {
			tikvNodeSetting := req.GetCluster().GetTikvNodeSetting()
			details[auditlog.KeyTikvSize] = tikvNodeSetting.GetNodeSpecKey()
			details[auditlog.KeyTikvQuantity] = tikvNodeSetting.GetNodeCount()
			details[auditlog.KeyTikvStorageSize] = tikvNodeSetting.GetStorageSizeGi()
			if tikvNodeSetting.GetStorageType() != openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED {
				details[auditlog.KeyTikvStorageType] = tikvNodeSetting.GetStorageType()
			}
			if tikvNodeSetting.GetDataStoreIops() > 0 {
				details[auditlog.KeyTikvDataStoreIops] = tikvNodeSetting.GetDataStoreIops()
			}
		}
		if req.GetCluster().GetTiflashNodeSetting() != nil {
			details[auditlog.KeyTiflashSize] = req.GetCluster().GetTiflashNodeSetting().GetNodeSpecKey()
			details[auditlog.KeyTiflashQuantity] = req.GetCluster().GetTiflashNodeSetting().GetNodeCount()
			details[auditlog.KeyTiflashStorageSize] = req.GetCluster().GetTiflashNodeSetting().GetStorageSizeGi()
		}

		return details
	}
	defer func() {
		if !req.GetValidateOnly() {
			c.auditLogSender.SubmitAuditLog(ctx, auditlog.EventCreateCluster, reqMeta, detailsFunc, err)
		}
	}()

	if req.Cluster == nil {
		err = errors.New("cluster object should not be nil")
		return nil, bizerr.WrapErrorWithCode(err, 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")
	}

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

	// use project id from request label if exists
	if req.Cluster.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,
				)
			}
		}
	}

	if c.oneConsoleConfig != nil && c.oneConsoleConfig.CheckXProjectType {
		project, err := c.accountCli.GetProject(ctx, projectId)

		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to get project")
		}
		if project.TenantId != reqMeta.OrgID {
			return nil, bizerr.New(bizerr.ErrCodeParams, "project does not belong to the current organization")
		}
		if project.GetType() == accountsdk.ProjectType_PROJECT_TYPE_TIDBXVIRTUAL ||
			project.GetType() == accountsdk.ProjectType_PROJECT_TYPE_TIDBX {
			return nil, bizerr.New(bizerr.ErrCodeParams, "dedicated cluster must be created in a dedicated project")
		}
	}

	// Parse the region field.
	var provider, region string
	provider, region, err = common.ParseOpenApiRegionNameOrId(req.GetCluster().GetRegionId())
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region, please check and try again")
	}

	// Check cluster name format
	if !common.CheckDisplayNameFmt(req.Cluster.DisplayName) {
		err := fmt.Errorf("cluster name not allowed: %s", req.Cluster.DisplayName)
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid display name")
	}

	globalProvider, err := enum.FromString[globalcommonv1.CloudProvider_Name](provider)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "provider %s is not supported", provider)
	}

	globalRegion, err := enum.FromString[globalcommonv1.Region_Name](region)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "region %s is not supported", region)
	}

	versionResp, err := c.dedicatedCli.GetDefaultTidbVersion(ctx, &globaldedicatedv1.GetDefaultTidbVersionRequest{
		CloudProvider: globalcommonv1.CloudProvider_Name(globalProvider),
		Region:        globalcommonv1.Region_Name(globalRegion),
		TenantId:      reqMeta.OrgID,
	})
	if err != nil {
		tilog.Warn(ctx, "failed to get default tidb version", zap.Error(err))
	}

	var cluster *entity.Cluster
	// First build create cluster request
	cluster, err = c.buildCreateCluster(ctx, projectId, provider, region, versionResp.GetVersion(), reqMeta, req)
	if err != nil {
		return nil, err
	}

	// Then create cluster in domain service
	cluster, err = c.clusterSvc.CreateCluster(ctx, cluster, nil, req.GetValidateOnly())
	if err != nil {
		if errors.Is(err, constant.ErrDuplicatedClusterName) {
			// Check cluster name is unique under the project
			return nil, bizerror.NewDisplayNameAlreadyExists(err)
		}
		return nil, err
	}
	clusterID = cluster.ClusterId

	if !req.GetValidateOnly() && 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
	}

	var respCluster *openapidedicatedv1beta1.Cluster
	respCluster, err = buildClusterFromEntity(cluster, nil)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to get cluster")
	}

	return respCluster, nil
}

func (c *serverImpl) buildCreateCluster(
	ctx context.Context,
	prjID uint64,
	provider, region, version string,
	reqMeta *metadata.RequestMeta,
	req *openapidedicatedv1beta1.CreateClusterRequest,
) (*entity.Cluster, bizerr.BizError) {
	globalProvider, err := enum.FromString[globalcommonv1.CloudProvider_Name](provider)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "provider %s is not supported", provider)
	}
	globalRegion, err := enum.FromString[globalcommonv1.Region_Name](region)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "region %s is not supported", region)
	}

	createCluster := &entity.Cluster{
		ProjectId:      prjID,
		OrganizationId: reqMeta.OrgID,
		DisplayName:    req.Cluster.DisplayName,
		UserId:         reqMeta.UserID,
		Labels:         req.Cluster.Labels,
		DeployType:     entity.DeployTypeDedicated,
		CloudProvider:  openapicommonv1beta1.Region_CloudProvider(globalProvider),
		Region:         region,
		ServicePlan:    openapicommonv1beta1.ServicePlan_Dedicated,
		CreatedBy:      common.BuildCreatedBy(reqMeta.Email, reqMeta.ApiPublicKey),
	}

	dedicatedCluster := &globaldedicatedv1.Cluster{
		OrganizationId:    reqMeta.OrgID,
		ProjectId:         prjID,
		CloudProviderId:   globalcommonv1.CloudProvider_Name(globalProvider),
		RegionId:          globalcommonv1.Region_Name(globalRegion),
		Port:              uint32(req.Cluster.Port),
		RootPassword:      req.Cluster.RootPassword,
		ClusterComponents: make([]*globaldedicatedv1.Cluster_ClusterComponent, 0),
	}

	if len(req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tidb node groups is required")
	}

	if len(req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()) > 1 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "unable to set multiple tidb node groups when creating a cluster")
	}

	tikvNodeSpec, tiflashNodeSpec, berr := appcommon.GetCreateNodeSpecs(
		ctx,
		c.regionApp,
		req.GetCluster().GetRegionId(),
		req.GetCluster().GetTikvNodeSetting().GetNodeSpecKey(),
		req.GetCluster().GetTiflashNodeSetting().GetNodeSpecKey(),
		strconv.FormatUint(prjID, 10),
		version,
	)
	if berr != nil {
		return nil, berr
	}

	comps, err_ := appcommon.BuildCreateClusterComponents(
		req.GetCluster().GetTidbNodeSetting().GetNodeSpecKey(),
		req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()[0].GetTiproxySetting().GetNodeSpecKey(),
		version,
		req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()[0].GetNodeCount(),
		req.GetCluster().GetTikvNodeSetting().GetNodeCount(),
		req.GetCluster().GetTiflashNodeSetting().GetNodeCount(),
		req.GetCluster().GetTidbNodeSetting().GetTidbNodeGroups()[0].GetTiproxySetting().GetNodeCount(),
		req.GetCluster().GetTikvNodeSetting().GetStorageType(),
		req.GetCluster().GetTiflashNodeSetting().GetStorageType(),
		req.GetCluster().GetTikvNodeSetting().GetStorageSizeGi(),
		req.GetCluster().GetTiflashNodeSetting().GetStorageSizeGi(),
		req.GetCluster().GetTikvNodeSetting().GetRaftStoreIops(),
		req.GetCluster().GetTikvNodeSetting().GetDataStoreIops(),
		openapicommonv1beta1.Region_CloudProvider(globalProvider),
		tikvNodeSpec,
		tiflashNodeSpec,
	)
	if err_ != nil {
		return nil, err_
	}

	dedicatedCluster.ClusterComponents = comps

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

	createCluster.GlobalCluster = dedicatedCluster
	return createCluster, nil
}
