package cluster

import (
	"context"
	"fmt"
	"strconv"
	"strings"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	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"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	appcommon "github.com/tidbcloud/tidb-management-service/tidb-mgmt-service/app/common"
	"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) RestoreDedicatedCluster(
	ctx context.Context,
	request *tidbmgmtservicev1.RestoreDedicatedClusterRequest,
) (*tidbmgmtservicev1.Cluster, error) {
	if request.GetCreatedBy() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "created by is empty")
	}
	if request.GetOrganizationId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "organization id is invalid")
	}
	if request.GetProjectId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "project id is invalid")
	}
	if request.GetCloudProvider() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cloud provider is empty")
	}
	if request.GetRegion() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "region is empty")
	}
	if request.GetClusterName() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster name is empty")
	}
	if request.GetPort() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "port is invalid")
	}

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

	requestMeta := &metadata.RequestMeta{
		OrgID:            request.GetOrganizationId(),
		DefaultProjectID: request.GetProjectId(),
		ValidProjectList: []uint64{request.GetProjectId()},
	}
	requestMeta, err := c.fillUserInfo(ctx, requestMeta, request.GetCreatedBy())
	if err != nil {
		return nil, err
	}

	// gen request meta and append to incoming meta
	ctx = metadata.AppendToIncomingMeta(ctx, requestMeta)

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

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

	_, regionId, _, err := common.BuildOpenApiRegionName(request.GetCloudProvider(), request.GetRegion())
	if err != nil {
		return nil, bizerr.WrapError(err, "invalid region info from cluster")
	}

	tikvNodeSpec, tiflashNodeSpec, err := appcommon.GetCreateNodeSpecs(
		ctx,
		c.openApiDedicatedRegionApp,
		regionId,
		request.GetTikvComp().GetSpuName(),
		request.GetTiflashComp().GetSpuName(),
		strconv.FormatUint(request.GetProjectId(), 10),
		request.GetVersion(),
	)
	if err != nil {
		return nil, err
	}

	comps, err_ := appcommon.BuildCreateClusterComponents(
		request.GetTidbComp().GetSpuName(),
		request.GetTiproxyComp().GetNodeSpecKey(),
		request.GetVersion(),
		request.GetTidbComp().GetCount(),
		request.GetTikvComp().GetCount(),
		request.GetTiflashComp().GetCount(),
		request.GetTiproxyComp().GetNodeCount(),
		request.GetTikvComp().GetStorageType(),
		request.GetTiflashComp().GetStorageType(),
		request.GetTikvComp().GetSizeGi(),
		request.GetTiflashComp().GetSizeGi(),
		request.GetTikvComp().GetRaftIops(),
		request.GetTikvComp().GetDataStoreIops(),
		openapicommonv1beta1.Region_CloudProvider(globalProvider),
		tikvNodeSpec,
		tiflashNodeSpec,
	)
	if err_ != nil {
		return nil, err_
	}

	// Set additional_iops on DATA storage for TiKV and TiFlash
	for _, comp := range comps {
		var additionalIops int32
		switch comp.ComponentType {
		case globaldedicatedv1.ComponentType_TIKV:
			additionalIops = request.GetTikvComp().GetAdditionalIops()
		case globaldedicatedv1.ComponentType_TIFLASH:
			additionalIops = request.GetTiflashComp().GetAdditionalIops()
		}
		if additionalIops > 0 {
			if dataStorage, ok := comp.Storages[globaldedicatedv1.Sku_DATA.String()]; ok {
				v := uint32(additionalIops)
				dataStorage.AdditionalIops = &v
			}
		}
	}

	dedicatedCluster := &globaldedicatedv1.Cluster{
		OrganizationId:    request.GetOrganizationId(),
		ProjectId:         request.GetProjectId(),
		CloudProviderId:   globalcommonv1.CloudProvider_Name(globalProvider),
		RegionId:          globalcommonv1.Region_Name(globalRegion),
		Port:              uint32(request.GetPort()),
		RootPassword:      request.GetRootPassword(),
		ClusterComponents: comps,
		Version:           request.GetVersion(),
	}

	// full global cluster
	restoreCluster := &entity.Cluster{
		ProjectId:      request.GetProjectId(),
		OrganizationId: request.GetOrganizationId(),
		DisplayName:    request.GetClusterName(),
		Region:         request.GetRegion(),
		CloudProvider:  openapicommonv1beta1.Region_CloudProvider(globalProvider),
		CreatedBy:      getCreatedBy(requestMeta, request.GetCreatedBy()),
		DeployType:     entity.DeployTypeDedicated,
		UserId:         requestMeta.UserID,
		Labels: map[string]string{
			common.ProjectIDLabelKey: strconv.FormatUint(request.GetProjectId(), 10),
			common.OrgIDLabelKey:     strconv.FormatUint(request.GetOrganizationId(), 10),
			metadata.UserIDHeaderKey: requestMeta.UserID,
		},
		GlobalCluster: dedicatedCluster,
		ServicePlan:   openapicommonv1beta1.ServicePlan_Dedicated,
	}

	// restore cluster
	cluster, err := c.dedicatedClusterSvc.RestoreCluster(
		ctx,
		restoreCluster,
		request.GetSourceClusterId(),
		request.GetRestoreId(),
		request.GetValidateOnly(),
		request.GetClusterConfiguration(),
	)
	if err != nil {
		return nil, err
	}

	cstatus, err := c.customStatusSvc.ListClusterCustomStatus(ctx, []uint64{cluster.ClusterId}, nil, false)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}

	return cvtToMgmtCluster(cluster, cstatus[cluster.ClusterId]), nil
}

func (c *serverImpl) RestoreServerlessCluster(
	ctx context.Context,
	request *tidbmgmtservicev1.RestoreServerlessClusterRequest,
) (*tidbmgmtservicev1.Cluster, error) {
	if request == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "restore cluster is nil")
	}

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

	requestMeta := &metadata.RequestMeta{
		OrgID:            request.GetOrganizationId(),
		DefaultProjectID: request.GetProjectId(),
		ValidProjectList: []uint64{request.GetProjectId()},
	}
	requestMeta, err := c.fillUserInfo(ctx, requestMeta, request.GetCreatedBy())
	if err != nil {
		return nil, err
	}

	// gen request meta and append to incoming meta
	ctx = metadata.AppendToIncomingMeta(ctx, requestMeta)

	openapiProvider, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](request.GetCloudProvider())
	if err != nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "provider %s is not supported", request.GetCloudProvider())
	}

	// full global cluster
	restoreCluster := &entity.Cluster{
		ProjectId:      request.GetProjectId(),
		OrganizationId: request.GetOrganizationId(),
		DisplayName:    request.GetClusterName(),
		Region:         request.GetRegion(),
		CloudProvider:  openapicommonv1beta1.Region_CloudProvider(openapiProvider),
		CreatedBy:      getCreatedBy(requestMeta, request.GetCreatedBy()),
		DeployType:     entity.DeployTypeServerless,
		UserId:         requestMeta.UserID,
		Labels: map[string]string{
			common.ProjectIDLabelKey: strconv.FormatUint(request.GetProjectId(), 10),
			common.OrgIDLabelKey:     strconv.FormatUint(request.GetOrganizationId(), 10),
			metadata.UserIDHeaderKey: requestMeta.UserID,
		},
		ServicePlan: openapicommonv1beta1.ServicePlan_Starter,
	}

	globalCluster := &globalserverlessv1.ServerlessServiceCluster{
		TenantId:      request.GetOrganizationId(),
		ProjectId:     request.GetProjectId(),
		CloudProvider: request.GetCloudProvider(),
		Region:        request.GetRegion(),
		SpendLimit:    request.GetSpendingLimit(),
		BackupPolicy:  request.GetAutomatedBackupPolicy(),
		Cluster: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
			EnhancedEncryptionEnabled: request.GetEncryptionConfig().GetEnhancedEncryptionEnabled(),
			RestoreCluster:            true,
			Kubernetes:                request.GetKubernetes(),
			Endpoints: &regionalserverlessv1.Endpoints{
				Public: &regionalserverlessv1.PublicEndpoint{
					Disabled: request.GetDisablePublicEndpoint(),
				},
			},
			ZoneRedundantEnabled: request.GetZoneRedundantEnabled(),
		},
	}

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

	restoreCluster.GlobalCluster = globalCluster

	// restore cluster
	cluster, err := c.serverlessClusterSvc.CreateCluster(ctx, restoreCluster)
	if err != nil {
		return nil, err
	}

	cstatus, err := c.customStatusSvc.ListClusterCustomStatus(ctx, []uint64{cluster.ClusterId}, nil, false)
	if err != nil {
		return nil, status.Error(codes.Internal, err.Error())
	}
	return cvtToMgmtCluster(cluster, cstatus[cluster.ClusterId]), nil
}

func (c *serverImpl) fillUserInfo(
	ctx context.Context,
	reqMeta *metadata.RequestMeta,
	createdBy string,
) (*metadata.RequestMeta, error) {
	// set email and user id
	switch {
	case common.ValidMailAddress(createdBy):
		user, err := c.accountCli.GetUserByEmail(ctx, createdBy)
		if err != nil || user == nil {
			return nil, bizerror.NewInternal(err, "failed to get user by email")
		}
		reqMeta.UserID = strconv.FormatUint(user.Id, 10)
		reqMeta.Email = user.Email
	case strings.HasPrefix(createdBy, "apikey-"):
		publicKey := strings.TrimPrefix(createdBy, "apikey-")
		oaAccess, err := c.accountCli.GetOpenAPIAccess(ctx, publicKey)
		if err != nil || oaAccess == nil {
			return nil, bizerror.NewInternal(err, "failed to get openapi access by public key")
		}
		user, err := c.accountCli.GetOrCreateSystemUser(ctx, oaAccess.Org.Id)
		if err != nil || user == nil {
			return nil, bizerror.NewInternal(err, "failed to get user by org id")
		}
		reqMeta.UserID = strconv.FormatUint(user.Id, 10)
		reqMeta.Email = user.Email
	default:
		// core svc only support set user id
		userId, err := strconv.ParseUint(createdBy, 10, 64)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid user id")
		}
		user, err := c.accountCli.GetUserById(ctx, userId)
		if err != nil || user == nil {
			return nil, bizerror.NewInternal(err, "failed to get user by id")
		}
		reqMeta.UserID = strconv.FormatUint(user.Id, 10)
		reqMeta.Email = user.Email
	}

	return reqMeta, nil
}

func getCreatedBy(reqMeta *metadata.RequestMeta, createdBy string) string {
	if strings.HasPrefix(createdBy, "apikey-") {
		return createdBy
	}
	return reqMeta.Email
}
