package cluster

import (
	"context"
	"errors"
	"slices"
	"strconv"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"
	"gorm.io/gorm"

	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/domain/repository"
	"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"
)

func (c *serverImpl) ListClusters(
	ctx context.Context,
	req *openapidedicatedv1beta1.ListClustersRequest,
) (*openapidedicatedv1beta1.ListClustersResponse, error) {
	// Extract request meta.
	reqMeta, err := metadata.ExtractRequestMetaWithCheckPermission(ctx)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "failed to extract request metadata")
	}

	var projectIds []uint64
	if req.GetProjectId() != "" {
		var projectId uint64
		projectId, err = strconv.ParseUint(req.GetProjectId(), 10, 64)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid project id")
		}
		projectIds = append(projectIds, projectId)
	} else {
		projectIds = reqMeta.ValidProjectList
	}

	tilog.Info(ctx, "Listing clusters with project IDs", zap.Uint64s("project_ids", projectIds))
	var filterClusterIds []uint64
	if req.GetClusterIds() != nil {
		filterClusterIds = make([]uint64, 0, len(req.GetClusterIds()))
		for _, clusterId := range req.GetClusterIds() {
			id, err := strconv.ParseUint(clusterId, 10, 64)
			if err != nil {
				return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id")
			}
			filterClusterIds = append(filterClusterIds, id)
		}
	}

	view := entity.ClusterViewClusterViewFull
	if req.GetGlobalMetaOnly() {
		view = entity.ClusterViewClusterViewBasic
	}

	// Create empty request.
	clusterFilter := &repository.ClusterFilter{
		Labels:       make(map[string]string),
		DeployType:   entity.DeployTypeDedicated,
		View:         view,
		QueryDeleted: false,
		ProjectIds:   projectIds,
		ClusterIds:   filterClusterIds,
		States:       req.GetClusterStates(),
	}

	if len(req.GetTags()) > 0 {
		tags, tagErr := entity.NormalizeCanonicalTags(req.GetTags())
		if tagErr != nil {
			return nil, bizerr.WrapErrorWithCode(tagErr, bizerr.ErrCodeParams, "invalid tags")
		}
		clusterFilter.TagCanonicalNames = entity.CanonicalTags(tags)
	}

	for _, regionId := range req.GetRegionIds() {
		provider, region, err := common.ParseOpenApiRegionNameOrId(regionId)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region id")
		}

		var includeProvider bool
		if slices.Contains(clusterFilter.CloudProviders, provider) {
			includeProvider = true
		}

		if !includeProvider {
			clusterFilter.CloudProviders = append(clusterFilter.CloudProviders, provider)
		}
		clusterFilter.Regions = append(clusterFilter.Regions, region)
	}

	// Validate the project
	if err := reqMeta.CheckProjectIDsPermission(clusterFilter.ProjectIds...); err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodePermission, bizerr.ErrMsgResourceNoPermission)
	}

	// first count cluster from domain service
	count, err := c.clusterSvc.CountClusters(ctx, clusterFilter)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to count clusters")
	}

	// not need to query domain service if cluster count equals 0
	if count == 0 {
		return &openapidedicatedv1beta1.ListClustersResponse{
			Clusters:      []*openapidedicatedv1beta1.Cluster{},
			TotalSize:     int32(count),
			NextPageToken: "",
		}, nil
	}

	// then query cluster from domain service
	clusters, nextPageToken, err := common.RemotePagination(func(page common.Page) ([]*entity.Cluster, error) {
		cs, err := c.clusterSvc.ListClusters(ctx, clusterFilter, &page)
		if err != nil {
			return nil, bizerror.NewInternal(err, "failed to query clusters")
		}

		// sort by create time desc
		slices.SortFunc(cs, func(a, b *entity.Cluster) int {
			if a.CreatedAt.After(b.CreatedAt) {
				return -1
			}
			if a.CreatedAt.Before(b.CreatedAt) {
				return 1
			}
			return 0
		})

		return cs, nil
	}, int(count), req, common.ListDefaultPageSize, common.ListMaxPageSize)
	if err != nil {
		return nil, bizerror.NewInternal(err, "failed to list clusters")
	}

	enrichedClusters := make([]*openapidedicatedv1beta1.Cluster, 0, len(clusters))

	clusterIds := lo.Map(clusters, func(item *entity.Cluster, _ int) uint64 {
		return item.ClusterId
	})

	// Fetch custom status for later filling
	statusMap, err := c.customStatusSvc.ListClusterCustomStatus(ctx, clusterIds, nil, false)
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, bizerror.NewInternal(err, "failed to get cluster custom status record")
	}

	// Enrich the response with cluster entities.
	for _, cluster := range clusters {
		var enrichedCluster *openapidedicatedv1beta1.Cluster
		enrichedCluster, err = buildClusterFromEntity(cluster, statusMap[cluster.ClusterId])
		if err != nil {
			return nil, bizerror.NewInternal(err, "failed to get cluster")
		}
		enrichedClusters = append(enrichedClusters, enrichedCluster)
	}

	return &openapidedicatedv1beta1.ListClustersResponse{
		Clusters:      enrichedClusters,
		TotalSize:     int32(count),
		NextPageToken: nextPageToken,
	}, nil
}
