package cluster

import (
	"context"
	"errors"
	"strconv"

	"github.com/tidbcloud/tiinf/monitor/tilog"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"go.uber.org/zap"
	"gorm.io/gorm"

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

func (c *serverImpl) UpdateCluster(
	ctx context.Context,
	request *openapidedicatedv1beta1.UpdateClusterRequest,
) (*openapidedicatedv1beta1.Cluster, error) {
	ctx, span := tracer.Start(ctx, "UpdateCluster")
	defer span.End()
	var (
		err                 error
		reqMeta             *metadata.RequestMeta
		oldClusterComponent []*globaldedicatedv1.Cluster_ClusterComponent
		res                 *openapidedicatedv1beta1.Cluster
		req                 = request.GetCluster()
	)

	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeDedicated)
		details = eventbus.AppendDedicatedClusterDetails(details, res)

		if req.GetTidbNodeSetting() != nil {
			details[auditlog.KeyNewTidbSize] = res.GetTidbNodeSetting().GetNodeSpecKey()
			for _, group := range res.GetTidbNodeSetting().GetTidbNodeGroups() {
				if _, ok := details[auditlog.KeyNewTidbQuantity]; !ok {
					details[auditlog.KeyNewTidbQuantity] = int32(0)
				}
				// count all tidb node
				// not record the tidb group id yet
				details[auditlog.KeyNewTidbQuantity] = details[auditlog.KeyNewTidbQuantity].(int32) + group.GetNodeCount()
				if group.GetTiproxySetting() != nil {
					if group.GetTiproxySetting().GetNodeSpecKey() != "" {
						details[auditlog.KeyNewTiProxySize] = group.GetTiproxySetting().GetNodeSpecKey()
					}
					if _, ok := details[auditlog.KeyNewTiProxyQuantity]; !ok {
						details[auditlog.KeyNewTiProxyQuantity] = int32(0)
					}
					details[auditlog.KeyNewTiProxyQuantity] = details[auditlog.KeyNewTiProxyQuantity].(int32) + group.GetTiproxySetting().
						GetNodeCount()
				}
			}
		}

		if req.GetTikvNodeSetting() != nil {
			tikvNodeSetting := res.GetTikvNodeSetting()
			details[auditlog.KeyNewTikvSize] = tikvNodeSetting.GetNodeSpecKey()
			details[auditlog.KeyNewTikvQuantity] = tikvNodeSetting.GetNodeCount()
			details[auditlog.KeyNewTikvStorageSize] = tikvNodeSetting.GetStorageSizeGi()
			if tikvNodeSetting.GetStorageType() != openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED {
				details[auditlog.KeyNewTikvStorageType] = tikvNodeSetting.GetStorageType().String()
			}
			if tikvNodeSetting.GetDataStoreIops() > 0 {
				details[auditlog.KeyNewTikvDataStoreIops] = tikvNodeSetting.GetDataStoreIops()
			}
		}

		if req.GetTiflashNodeSetting() != nil {
			details[auditlog.KeyNewTiflashSize] = res.GetTiflashNodeSetting().GetNodeSpecKey()
			details[auditlog.KeyNewTiflashQuantity] = res.GetTiflashNodeSetting().GetNodeCount()
			details[auditlog.KeyNewTiflashStorageSize] = res.GetTiflashNodeSetting().GetStorageSizeGi()
		}

		// fulfill old cluster component
		for _, component := range oldClusterComponent {
			switch component.ComponentType {
			case globaldedicatedv1.ComponentType_TIDB:
				details[auditlog.KeyOldTidbSize] = component.GetSpuName()
				if _, ok := details[auditlog.KeyOldTidbQuantity]; !ok {
					details[auditlog.KeyOldTidbQuantity] = uint32(0)
				}
				// count all tidb node
				// not record the tidb group id yet
				details[auditlog.KeyOldTidbQuantity] = details[auditlog.KeyOldTidbQuantity].(uint32) + component.GetCount()
			case globaldedicatedv1.ComponentType_TIKV:
				details[auditlog.KeyOldTikvSize] = component.GetSpuName()
				details[auditlog.KeyOldTikvQuantity] = component.Count
				details[auditlog.KeyOldTikvStorageSize] = component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetSizeGi()
				dataStorage := component.GetStorages()[globaldedicatedv1.Sku_DATA.String()]
				raftStorage := component.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()]
				storageType := storagetype.BuildCurrentTikvStorageType(
					dataStorage.GetVolumeType(),
					raftStorage.GetVolumeType(),
				)
				if req.GetTikvNodeSetting().GetStorageType() != openapidedicatedv1beta1.Cluster_StorageNodeSetting_STORAGE_TYPE_UNSPECIFIED {
					details[auditlog.KeyOldTikvStorageType] = storageType.String()
				}
				if storageType == openapidedicatedv1beta1.Cluster_StorageNodeSetting_Plus &&
					dataStorage.GetVolumeType() == globaldedicatedv1.StorageMu_IO2 &&
					dataStorage.GetIops() > 0 {
					details[auditlog.KeyOldTikvDataStoreIops] = dataStorage.GetIops()
				}
			case globaldedicatedv1.ComponentType_TIFLASH:
				details[auditlog.KeyOldTiflashSize] = component.GetSpuName()
				details[auditlog.KeyOldTiflashQuantity] = component.Count
				details[auditlog.KeyOldTiflashStorageSize] = component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetSizeGi()
			case globaldedicatedv1.ComponentType_TIPROXY:
				details[auditlog.KeyOldTiProxySize] = component.GetSpuName()
				if _, ok := details[auditlog.KeyOldTiProxyQuantity]; !ok {
					details[auditlog.KeyOldTiProxyQuantity] = uint32(0)
				}
				details[auditlog.KeyOldTiProxyQuantity] = details[auditlog.KeyOldTiProxyQuantity].(uint32) + component.GetCount()
			}
		}
		return details
	}
	defer func() {
		if !request.GetValidateOnly() {
			c.auditLogSender.SubmitAuditLog(ctx, auditlog.EventScaleCluster, reqMeta, detailsFunc, err)
		}
	}()

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

	cluster := &entity.Cluster{}
	cluster.ClusterId, err = strconv.ParseUint(req.GetClusterId(), 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}

	if req.GetDisplayName() != "" {
		cluster.DisplayName = req.GetDisplayName()
	}

	// get global cluster info.
	globalCluster, err := c.dedicatedCli.GetCluster(
		ctx,
		&globaldedicatedv1.GetClusterRequest{ClusterId: cluster.ClusterId},
	)
	if err != nil {
		return nil, err
	}

	// get node spec.
	tikvNodeSpec, tiflashNodeSpec, err := appcommon.GetUpdateNodeSpecs(
		ctx,
		c.regionApp,
		req.GetTikvNodeSetting().GetNodeSpecKey(),
		req.GetTiflashNodeSetting().GetNodeSpecKey(),
		strconv.FormatUint(globalCluster.GetProjectId(), 10),
		globalCluster,
	)

	components, err := appcommon.BuildUpdateGlobalComponents(req, globalCluster, tikvNodeSpec, tiflashNodeSpec)
	if err != nil {
		return nil, err
	}

	// update from domain service
	var customStatus *entity.ClusterCustomStatus
	cluster, customStatus, oldClusterComponent, err = c.clusterSvc.UpdateCluster(
		ctx,
		cluster,
		components,
		false,
		request.GetValidateOnly(),
		"",
		nil,
	)
	if err != nil {
		switch {
		case errors.Is(err, gorm.ErrRecordNotFound):
			return nil, bizerror.NewNotFound(err, "cluster not found")
		case errors.Is(err, constant.ErrClusterCustomStatusForbidden):
			return nil, bizerror.NewForbiddenForClusterCustomStatus(err, customStatus.StatusType.String())
		case errors.Is(err, constant.ErrDuplicatedClusterName):
			// Check cluster name is unique under the project
			return nil, bizerror.NewDisplayNameAlreadyExists(err)
		default:
			return nil, err
		}
	}

	// check custom status
	var (
		customStatusMap map[uint64][]*entity.ClusterCustomStatus
		customStatuses  []*entity.ClusterCustomStatus
	)
	customStatusMap, err = c.customStatusSvc.ListClusterCustomStatus(ctx, []uint64{cluster.ClusterId}, nil, false)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		tilog.Warn(ctx, "failed to get custom status", zap.String("cluster_id", req.ClusterId), zap.Error(err))
	}
	customStatuses = customStatusMap[cluster.ClusterId]

	// override custom status
	res, err = buildClusterFromEntity(cluster, customStatuses)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to get cluster")
	}

	return res, nil
}
