package cluster

import (
	"context"
	"errors"

	"github.com/samber/lo"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"gorm.io/gorm"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	tidbmgmtservicev1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_mgmt_service/v1"
	"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/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/clusterevent"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

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

	if request.GetDiskType() == globaldedicatedv1.Sku_DISK_TYPE_UNSPECIFIED {
		return nil, bizerr.New(bizerr.ErrCodeParams, "storage type is invalid")
	}

	var forceScale bool
	if request.GetVolumeType() != globaldedicatedv1.StorageMu_VOLUME_TYPE_UNSPECIFIED {
		forceScale = true
	}

	component := &globaldedicatedv1.Cluster_ClusterComponent{
		ComponentType: request.ComponentType,
		Storages: map[string]*globaldedicatedv1.Cluster_ClusterComponent_Storage{
			request.GetDiskType().String(): {
				DiskType:        request.GetDiskType(),
				SizeGi:          request.GetStorageSize(),
				Iops:            request.GetStorageIops(),
				ThroughputMibps: request.GetStorageThroughput(),
				VolumeType:      request.VolumeType,
				AdditionalIops:  request.AdditionalIops,
			},
		},
	}

	return c.scaleDedicatedCluster(
		ctx,
		request.GetClusterId(),
		[]*globaldedicatedv1.Cluster_ClusterComponent{component},
		forceScale,
		string(clusterevent.OperationSourceModifyClusterVolume),
		modifyClusterVolumeRequestedFields(request),
	)
}

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

	if request.GetSpuName() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tier name is required")
	}

	component := &globaldedicatedv1.Cluster_ClusterComponent{
		ComponentType: globaldedicatedv1.ComponentType_PD,
		SpuName:       request.GetSpuName(),
	}

	return c.scaleDedicatedCluster(
		ctx,
		request.GetClusterId(),
		[]*globaldedicatedv1.Cluster_ClusterComponent{component},
		false,
		string(clusterevent.OperationSourceScaleClusterPdComponent),
		[]string{"component_type", "tier_name"},
	)
}

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

	if request.GetSpuName() == "" {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tier name is required")
	}

	component := &globaldedicatedv1.Cluster_ClusterComponent{
		ComponentType: globaldedicatedv1.ComponentType_TIPROXY,
		SpuName:       request.GetSpuName(),
		Count:         lo.ToPtr(request.GetCount()),
		TidbGroupId:   request.GetTidbNodeGroupId(),
	}

	return c.scaleDedicatedCluster(
		ctx,
		request.GetClusterId(),
		[]*globaldedicatedv1.Cluster_ClusterComponent{component},
		false,
		string(clusterevent.OperationSourceScaleClusterTiProxy),
		[]string{"component_type", "quantity", "tidb_group_id", "tier_name"},
	)
}

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

	return c.scaleDedicatedCluster(
		ctx,
		request.GetClusterId(),
		request.GetClusterComponents(),
		request.GetForceScale(),
		string(clusterevent.OperationSourceScaleClusterComponents),
		nil,
	)
}

func (c *serverImpl) scaleDedicatedCluster(
	ctx context.Context,
	clusterId uint64,
	clusterComponents []*globaldedicatedv1.Cluster_ClusterComponent,
	forceScale bool,
	sourceOperation string,
	requestedFields []string,
) (*tidbmgmtservicev1.Cluster, error) {
	if clusterId == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster id is invalid")
	}

	var (
		cluster *entity.Cluster
		err     error
	)

	cluster, err = c.dedicatedClusterSvc.GetCluster(ctx, clusterId, entity.ClusterViewClusterViewLocal, false, false)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "dedicated cluster not found")
		}
		return nil, bizerror.NewInternal(err, "failed to get dedicated cluster")
	}

	// gen request meta and append to outgoing meta
	ctx = metadata.AppendToOutgoingMeta(ctx, &metadata.RequestMeta{
		OrgID:            cluster.OrganizationId,
		DefaultProjectID: cluster.ProjectId,
		ValidProjectList: []uint64{cluster.ProjectId},
		ClusterID:        clusterId,
		UserID:           "0",
	})

	// Scale from domain service
	var customStatus *entity.ClusterCustomStatus
	cluster, customStatus, _, err = c.dedicatedClusterSvc.UpdateCluster(
		ctx,
		cluster,
		clusterComponents,
		forceScale,
		false,
		sourceOperation,
		requestedFields,
	)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "cluster not found")
		} else if errors.Is(err, constant.ErrClusterCustomStatusForbidden) {
			return nil, bizerror.NewForbiddenForClusterCustomStatus(err, customStatus.StatusType.String())
		}
		return nil, err
	}

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

func modifyClusterVolumeRequestedFields(request *tidbmgmtservicev1.ModifyClusterVolumeRequest) []string {
	fields := []string{"component_type", "disk_type"}
	if request.StorageSize != nil {
		fields = append(fields, "storage_size")
	}
	if request.StorageIops != nil {
		fields = append(fields, "storage_iops")
	}
	if request.StorageThroughput != nil {
		fields = append(fields, "storage_throughput")
	}
	if request.VolumeType != nil {
		fields = append(fields, "volume_type")
	}
	if request.AdditionalIops != nil {
		fields = append(fields, "additional_iops")
	}
	return fields
}
