package cluster

import (
	"cmp"
	"context"
	"errors"
	"fmt"
	"slices"
	"strconv"

	"braces.dev/errtrace"
	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"google.golang.org/protobuf/types/known/timestamppb"
	"gorm.io/gorm"

	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"
	"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/grpc/metadata"
	"github.com/tidbcloud/tidb-management-service/utilities/spu/storagetype"
)

func (c *serverImpl) GetCluster(
	ctx context.Context,
	req *openapidedicatedv1beta1.GetClusterRequest,
) (*openapidedicatedv1beta1.Cluster, error) {
	var err error
	cluster := &entity.Cluster{}
	cluster.ClusterId, err = strconv.ParseUint(req.ClusterId, 10, 64)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
	}

	// get from domain service.
	// Unlike ListClusters (which maps global_meta_only → BasicView → ShowDetails=false),
	// domain GetCluster ignores view for Regional enrichment: Basic and Full both fetch
	// Regional details today. Skipping Regional is controlled only by globalMetaOnly
	// (→ global ignore_details). Do not switch to BasicView here—it would not skip
	// Regional on the Get path and would diverge from original behavior.
	cluster, err = c.clusterSvc.GetCluster(
		ctx,
		cluster.ClusterId,
		entity.ClusterViewClusterViewFull,
		false,
		req.GetGlobalMetaOnly(),
	)
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return nil, bizerror.NewNotFound(err, "cluster not found")
		}
		return nil, bizerror.NewInternal(err, "failed to get cluster")
	}

	// 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]

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

func buildClusterFromEntity(
	cluster *entity.Cluster,
	customStatuses []*entity.ClusterCustomStatus,
) (*openapidedicatedv1beta1.Cluster, error) {
	if cluster == nil {
		return nil, errtrace.Wrap(errors.New("cluster is nil"))
	}
	var (
		err              error
		dedicatedCluster *globaldedicatedv1.Cluster
	)

	if cluster.GlobalCluster != nil {
		dedicatedCluster, err = cluster.GetDedicatedCluster()
		if err != nil {
			return nil, errtrace.Wrap(err)
		}
	}

	clusterID := strconv.FormatUint(cluster.ClusterId, 10)
	result := openapidedicatedv1beta1.Cluster{
		Name:        fmt.Sprintf("clusters/%s", clusterID),
		ClusterId:   clusterID,
		DisplayName: cluster.DisplayName,
		TidbNodeSetting: &openapidedicatedv1beta1.Cluster_TidbNodeSetting{
			TidbNodeGroups: []*openapidedicatedv1beta1.TidbNodeGroup{},
		},
		CreateTime: timestamppb.New(cluster.CreatedAt),
		UpdateTime: timestamppb.New(cluster.UpdatedAt),
	}

	result.CreatedBy = common.ConvCreatedBy(cluster.CreatedBy)
	result.Labels = cluster.Labels
	result.Annotations = cluster.Annotations
	result.Tags = common.CvtEntityTagsToOpenAPITags(cluster.Tags)
	if result.Labels == nil {
		result.Labels = make(map[string]string)
	}
	result.Labels[common.ProjectIDLabelKey] = strconv.FormatUint(cluster.ProjectId, 10)
	result.Labels[common.OrgIDLabelKey] = strconv.FormatUint(cluster.OrganizationId, 10)

	result.RegionId = fmt.Sprintf("%s-%s", cluster.CloudProvider, cluster.Region)
	result.CloudProvider = cluster.CloudProvider

	if dedicatedCluster != nil {
		result.Port = int32(dedicatedCluster.Port)
		result.Version = dedicatedCluster.Version
		result.State = cvtState(dedicatedCluster.State, dedicatedCluster.HealthState)
		// if serviceRecord.Cluster.DeleteTs != 0 {
		// 	result.DeleteTime = timestamppb.New(time.Unix(serviceRecord.Cluster.DeleteTs, 0))
		// }

		if dedicatedCluster.UpdateTime != nil {
			result.UpdateTime = dedicatedCluster.UpdateTime
		}

		if dedicatedCluster.RegionDisplayName != "" {
			result.RegionDisplayName = dedicatedCluster.RegionDisplayName
		} else {
			result.RegionDisplayName = cluster.Region
		}

		groupEndpointMap := make(map[uint64]*globaldedicatedv1.TidbNodeGroup_Endpoint)
		if dedicatedCluster.TidbNodeGroupEndpoints != nil {
			for _, tidbGroupEndpoint := range dedicatedCluster.GetTidbNodeGroupEndpoints() {
				groupEndpointMap[tidbGroupEndpoint.GetTidbGroupId()] = tidbGroupEndpoint
			}
		}

		if dedicatedCluster.GetState() == globaldedicatedv1.Cluster_PAUSED ||
			dedicatedCluster.GetState() == globaldedicatedv1.Cluster_PAUSING {
			result.PausePlan = &openapidedicatedv1beta1.Cluster_PausePlan{
				PauseType:           cvtPauseType(dedicatedCluster.GetPausePlan().GetPausePlan()),
				ScheduledResumeTime: dedicatedCluster.GetPausePlan().GetScheduledResumeTime(),
			}
		}

		tidbGroups := make(map[uint64]*openapidedicatedv1beta1.TidbNodeGroup)

		for _, component := range dedicatedCluster.GetClusterComponents() {
			switch component.ComponentType {
			case globaldedicatedv1.ComponentType_TIDB:
				// fixme(@yifan): heterogeneous group.
				result.TidbNodeSetting.NodeSpecKey = component.GetSpuName()
				result.TidbNodeSetting.NodeSpecDisplayName = component.GetSpuDisplayName()
				result.TidbNodeSetting.NodeSpecVersion = component.GetSpuVersion()

				group := &openapidedicatedv1beta1.TidbNodeGroup{
					Name:                fmt.Sprintf("tidbNodeGroups/%d", component.TidbGroupId),
					NodeCount:           int32(component.GetCount()),
					TidbNodeGroupId:     strconv.FormatUint(component.TidbGroupId, 10),
					IsDefaultGroup:      component.IsDefaultTidbGroup,
					DisplayName:         component.GetTidbGroupDisplayName(),
					ClusterId:           clusterID,
					NodeSpecKey:         component.GetSpuName(),
					NodeSpecDisplayName: component.GetSpuDisplayName(),
					NodeSpecVersion:     component.GetSpuVersion(),
				}

				switch dedicatedCluster.GetState() {
				case globaldedicatedv1.Cluster_CREATING:
					group.State = openapidedicatedv1beta1.TidbNodeGroup_MODIFYING
				case globaldedicatedv1.Cluster_RESUMING,
					globaldedicatedv1.Cluster_PAUSING,
					globaldedicatedv1.Cluster_PAUSED:
					group.State = openapidedicatedv1beta1.TidbNodeGroup_PAUSED
				default:
					group.State = openapidedicatedv1beta1.TidbNodeGroup_ACTIVE
				}

				if component.ModifyProgress != nil {
					group.NodeChangingProgress = &openapidedicatedv1beta1.Cluster_NodeChangingProgress{
						MatchingNodeSpecNodeCount:  component.ModifyProgress.UptodateQuantity,
						RemainingDeletionNodeCount: component.ModifyProgress.RemainingDeletionQuantity,
					}
					group.State = openapidedicatedv1beta1.TidbNodeGroup_MODIFYING
				}

				if endpoint, ok := groupEndpointMap[component.GetTidbGroupId()]; ok {
					group.Endpoints = []*openapidedicatedv1beta1.TidbNodeGroup_Endpoint{}
					if endpoint.GetPublicEndpoint() != nil {
						group.Endpoints = append(group.Endpoints, &openapidedicatedv1beta1.TidbNodeGroup_Endpoint{
							ConnectionType: openapidedicatedv1beta1.TidbNodeGroup_Endpoint_PUBLIC,
							Host:           endpoint.GetPublicEndpoint().GetHost(),
							Port:           int32(endpoint.GetPublicEndpoint().GetPort()),
						})
					}

					if endpoint.GetVpcPeeringEndpoint() != nil {
						group.Endpoints = append(group.Endpoints, &openapidedicatedv1beta1.TidbNodeGroup_Endpoint{
							ConnectionType: openapidedicatedv1beta1.TidbNodeGroup_Endpoint_VPC_PEERING,
							Host:           endpoint.GetVpcPeeringEndpoint().GetHost(),
							Port:           int32(endpoint.GetVpcPeeringEndpoint().GetPort()),
						})
					}

					edps := lo.Map(
						endpoint.GetPrivateLinkEndpoints(),
						func(item *globaldedicatedv1.TidbNodeGroup_Endpoint_PrivateLinkEndpoint, _ int) *openapidedicatedv1beta1.TidbNodeGroup_Endpoint {
							return &openapidedicatedv1beta1.TidbNodeGroup_Endpoint{
								ConnectionType: openapidedicatedv1beta1.TidbNodeGroup_Endpoint_PRIVATE_ENDPOINT,
								Host:           item.GetHost(),
								Port:           int32(item.GetPort()),
							}
						},
					)
					group.Endpoints = append(group.Endpoints, edps...)
				}
				tidbGroups[component.TidbGroupId] = group

			case globaldedicatedv1.ComponentType_TIKV:
				result.TikvNodeSetting = &openapidedicatedv1beta1.Cluster_StorageNodeSetting{
					NodeSpecKey:         component.GetSpuName(),
					NodeSpecDisplayName: component.GetSpuDisplayName(),
					NodeSpecVersion:     component.GetSpuVersion(),
					NodeCount:           int32(component.GetCount()),
					StorageSizeGi: int32(
						component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetSizeGi(),
					),
				}

				dataVt := component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType()
				raftVt := component.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetVolumeType()
				result.TikvNodeSetting.StorageType = storagetype.BuildCurrentTikvStorageType(dataVt, raftVt)

				var raftIops *int32
				// User can only get raft iops when storage type is performance.
				if result.TikvNodeSetting.StorageType == openapidedicatedv1beta1.Cluster_StorageNodeSetting_Performance {
					raftIops = lo.EmptyableToPtr(
						int32(component.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetIops()),
					)
				}
				result.TikvNodeSetting.RaftStoreIops = raftIops

				// User can only get data store iops for AWS clusters with Plus storage type.
				if result.TikvNodeSetting.StorageType == openapidedicatedv1beta1.Cluster_StorageNodeSetting_Plus &&
					dataVt == globaldedicatedv1.StorageMu_IO2 {
					result.TikvNodeSetting.DataStoreIops = lo.EmptyableToPtr(
						int32(component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetIops()),
					)
				}

				if component.ModifyProgress != nil {
					result.TikvNodeSetting.NodeChangingProgress = &openapidedicatedv1beta1.Cluster_NodeChangingProgress{
						MatchingNodeSpecNodeCount:  component.ModifyProgress.UptodateQuantity,
						RemainingDeletionNodeCount: component.ModifyProgress.RemainingDeletionQuantity,
					}
				}

			case globaldedicatedv1.ComponentType_TIFLASH:
				result.TiflashNodeSetting = &openapidedicatedv1beta1.Cluster_StorageNodeSetting{
					NodeSpecKey:         component.GetSpuName(),
					NodeSpecDisplayName: component.GetSpuDisplayName(),
					NodeSpecVersion:     component.GetSpuVersion(),
					NodeCount:           int32(component.GetCount()),
					StorageSizeGi: int32(
						component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetSizeGi(),
					),
					RaftStoreIops: lo.EmptyableToPtr(
						int32(component.GetStorages()[globaldedicatedv1.Sku_RAFT_LOG.String()].GetIops()),
					),
				}

				dataVt := component.GetStorages()[globaldedicatedv1.Sku_DATA.String()].GetVolumeType()
				result.TiflashNodeSetting.StorageType = storagetype.BuildCurrentTiflashStorageType(
					dataVt,
					globaldedicatedv1.StorageMu_VOLUME_TYPE_UNSPECIFIED,
				)

				if component.ModifyProgress != nil {
					result.TiflashNodeSetting.NodeChangingProgress = &openapidedicatedv1beta1.Cluster_NodeChangingProgress{
						MatchingNodeSpecNodeCount:  component.ModifyProgress.UptodateQuantity,
						RemainingDeletionNodeCount: component.ModifyProgress.RemainingDeletionQuantity,
					}
				}
			}
		}

		for _, component := range dedicatedCluster.GetClusterComponents() {
			if component.ComponentType != globaldedicatedv1.ComponentType_TIPROXY {
				continue
			}
			if _, ok := tidbGroups[component.TidbGroupId]; !ok {
				return nil, errtrace.Wrap(
					fmt.Errorf("tidb group not found for tidb group id %d", component.TidbGroupId),
				)
			}
			tidbGroups[component.TidbGroupId].TiproxySetting = &openapidedicatedv1beta1.TidbNodeGroup_TiProxySetting{
				NodeCount:           lo.ToPtr(int32(component.GetCount())),
				NodeSpecKey:         component.GetSpuName(),
				NodeSpecDisplayName: component.GetSpuDisplayName(),
				NodeSpecVersion:     component.GetSpuVersion(),
			}
			if component.ModifyProgress != nil {
				tidbGroups[component.TidbGroupId].TiproxyNodeChangingProgress = &openapidedicatedv1beta1.Cluster_NodeChangingProgress{
					MatchingNodeSpecNodeCount:  component.ModifyProgress.UptodateQuantity,
					RemainingDeletionNodeCount: component.ModifyProgress.RemainingDeletionQuantity,
				}
				tidbGroups[component.TidbGroupId].State = openapidedicatedv1beta1.TidbNodeGroup_MODIFYING
			}
		}

		result.TidbNodeSetting.TidbNodeGroups = lo.Values(tidbGroups)
		// sort tidb node groups, default group first and then by id
		slices.SortFunc(result.TidbNodeSetting.TidbNodeGroups, func(groupI, groupJ *openapidedicatedv1beta1.TidbNodeGroup) int {
			if groupI.IsDefaultGroup && !groupJ.IsDefaultGroup {
				return -1
			}
			if !groupI.IsDefaultGroup && groupJ.IsDefaultGroup {
				return 1
			}
			// Since we're using internally generated IDs which are guaranteed to be valid uint64s,
			// and the IDs come from our own system, we can safely ignore parse errors here
			id1, _ := strconv.ParseUint(groupI.TidbNodeGroupId, 10, 64)
			id2, _ := strconv.ParseUint(groupJ.TidbNodeGroupId, 10, 64)
			return cmp.Compare(id1, id2)
		})

		// Set additional annotations.
		if result.Annotations == nil {
			result.Annotations = make(map[string]string)
		}
		result.Annotations[common.AvailableFeaturesKey] = common.CvtAvailableFeature(dedicatedCluster.GetFeatures())
		result.Annotations[common.HasSetPwdAnnotationKey] = strconv.FormatBool(dedicatedCluster.HasRootPassword)
		if dedicatedCluster.GetInsufficientVmResource() {
			result.Annotations[common.InsufficientVmResourcesKey] = "true"
		}
	}

	delete(result.Labels, metadata.UserIDHeaderKey)

	// Override state with custom status
	result.State = common.BuildClusterState(result.State, customStatuses)

	return &result, nil
}

// cvt user state
func cvtState(
	in globaldedicatedv1.Cluster_State,
	health globaldedicatedv1.Cluster_HealthState,
) openapicommonv1beta1.ClusterState {
	if health == globaldedicatedv1.Cluster_HEALTH_STATE_UNSPECIFIED &&
		in == globaldedicatedv1.Cluster_STATE_UNSPECIFIED {
		return openapicommonv1beta1.ClusterState_INACTIVE
	}

	switch in {
	case globaldedicatedv1.Cluster_STATE_UNSPECIFIED:
		return openapicommonv1beta1.ClusterState_CLUSTER_STATE_UNSPECIFIED
	case globaldedicatedv1.Cluster_AVAILABLE:
		return openapicommonv1beta1.ClusterState_ACTIVE
	case globaldedicatedv1.Cluster_CREATING:
		return openapicommonv1beta1.ClusterState_CREATING
	case globaldedicatedv1.Cluster_MODIFYING,
		globaldedicatedv1.Cluster_SCALING:
		return openapicommonv1beta1.ClusterState_MODIFYING
	case globaldedicatedv1.Cluster_PAUSED:
		return openapicommonv1beta1.ClusterState_PAUSED
	case globaldedicatedv1.Cluster_PAUSING:
		return openapicommonv1beta1.ClusterState_PAUSING
	case globaldedicatedv1.Cluster_RESUMING:
		return openapicommonv1beta1.ClusterState_RESUMING
	case globaldedicatedv1.Cluster_DELETED:
		return openapicommonv1beta1.ClusterState_DELETED
	}
	return openapicommonv1beta1.ClusterState_CLUSTER_STATE_UNSPECIFIED
}

func cvtPauseType(in globaldedicatedv1.Cluster_PausePlan_Type) openapidedicatedv1beta1.Cluster_PausePlan_Type {
	switch in {
	case globaldedicatedv1.Cluster_PausePlan_PAUSE_TYPE_UNSPECIFIED:
		return openapidedicatedv1beta1.Cluster_PausePlan_DEPRECATE
	case globaldedicatedv1.Cluster_PausePlan_NORMAL:
		return openapidedicatedv1beta1.Cluster_PausePlan_NORMAL
		// case globaldedicatedv1.Cluster_PausePlan_LONG:
		// 	return openapidedicatedv1beta1.Cluster_PausePlan_LONG
	}
	return openapidedicatedv1beta1.Cluster_PausePlan_DEPRECATE
}
