package tidbxinstance

import (
	"context"
	"fmt"
	"slices"
	"strconv"
	"strings"
	"time"

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.einride.tech/aip/pagination"
	"go.uber.org/zap"

	bffv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/bff/cluster_management/v1beta2"
	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"
	openapingv1beta2 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/nextgen/v1beta2"
	openapiserverlessv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/serverless/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/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

func (s *TiDBServer) ListTidbs(
	ctx context.Context,
	request *bffv1beta2.ListTidbsRequest,
) (resp *bffv1beta2.ListTidbsResponse, err error) {
	startTime := time.Now()
	defer func() {
		recordQueryAPIMetrics(apiListTidbs, time.Since(startTime).Seconds(), err)
	}()

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

	// page and sort cluster from mgmt repo
	pageToken, err := pagination.ParsePageToken(request)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid page token")
	}

	pageSize := request.GetPageSize()
	if pageSize <= 0 {
		pageSize = common.ListDefaultPageSize
	}

	allScope, validProjectIds, validInstanceIds, err := s.accountCli.GetInstancePermissionScopeOfUser(ctx, reqMeta.OrgID, reqMeta.UserID)

	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodePermission, "failed to get instance permission scope of user")
	}

	servicePlans := lo.Map(request.ServicePlans, func(item openapicommonv1beta1.ServicePlan, index int) string {
		return item.String()
	})

	cloudProviders := lo.Map(request.CloudProviders, func(item openapicommonv1beta1.Region_CloudProvider, index int) string {
		return item.String()
	})

	engineTypes := normalizeEngineTypesFilter(request.EngineTypes)

	projectIds := make([]uint64, 0)
	for _, projectId := range request.ProjectIds {
		parsedProjectId, err := strconv.ParseUint(projectId, 10, 64)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeParams, "invalid project id")
		}
		projectIds = append(projectIds, parsedProjectId)
	}

	clusterIds := make([]uint64, 0)
	for _, clusterId := range request.TidbIds {
		parsedClusterId, err := strconv.ParseUint(clusterId, 10, 64)
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeParams, "invalid tidb id")
		}
		clusterIds = append(clusterIds, parsedClusterId)
	}

	regions := make([]string, 0)
	regionPairs := make([]repository.RegionPair, 0)
	for _, region := range request.RegionIds {
		providerStr, regionStr, err := common.ParseOpenApiRegionNameOrId(region)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid region, please check and try again")
		}

		if len(cloudProviders) > 0 && !lo.Contains(cloudProviders, providerStr) {
			// region provider is not in the specific cloudProviders, ignore
			continue
		}

		regionPairs = append(regionPairs, repository.RegionPair{
			CloudProvider: providerStr,
			Region:        regionStr,
		})
		regions = append(regions, regionStr)
	}

	clusterFilter := &repository.ClusterFilter{
		Name:            request.GetName(),
		OrganizationIds: []uint64{reqMeta.OrgID},
		ServicePlans:    servicePlans,
		EngineTypes:     engineTypes,
		CloudProviders:  cloudProviders,
		ClusterIds:      clusterIds,
		Regions:         regions,
		RegionPairs:     regionPairs,
		ProjectIds:      projectIds,
		PermissionFilter: &repository.PermissionFilter{
			All:        allScope,
			ProjectIds: validProjectIds,
			ClusterIds: validInstanceIds,
		},
	}

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

	// count
	count, err := s.clusterRepo.CountClusterByFilter(ctx, clusterFilter)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeDB, "failed to count clusters")
	}

	if pageToken.Offset >= count || count == 0 {
		return &bffv1beta2.ListTidbsResponse{
			Tidbs:         nil,
			TotalSize:     int32(count),
			NextPageToken: "",
		}, nil
	}

	allClusters, err := s.clusterRepo.QueryClusterByFilterAndPage(
		ctx,
		clusterFilter,
		int(pageToken.Offset),
		int(pageSize),
	)
	if err != nil {
		return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeDB, "failed to query clusters")
	}

	tilog.Info(ctx, "queried clusters from mgmt repo", zap.Int("num", len(allClusters)))

	var tidbs []*bffv1beta2.Tidb

	if request.GetViewMode() == bffv1beta2.ListTidbsRequest_MINI {
		// MINI mode: return only mgmt-service data, skip querying global services.
		// The repository already orders clusters by created_at DESC.
		tidbs = make([]*bffv1beta2.Tidb, 0, len(allClusters))
		for _, cluster := range allClusters {
			tidbs = append(tidbs, &bffv1beta2.Tidb{
				TidbId:            strconv.FormatUint(cluster.ClusterId, 10),
				ServicePlan:       cluster.ServicePlan,
				EngineType:        normalizeEngineType(cluster.EngineType),
				OrganizationId:    cluster.OrganizationId,
				ProjectId:         cluster.ProjectId,
				OriginalProjectId: cluster.OriginalProjectId,
			})
		}
	} else {
		// BASIC mode (default): query global services and return full details
		serverlessIds := lo.Map(lo.Filter(allClusters, func(item *entity.Cluster, _ int) bool {
			return item.DeployType == entity.DeployTypeServerless
		}), func(item *entity.Cluster, _ int) uint64 {
			return item.ClusterId
		})

		dedicatedIds := lo.Map(lo.Filter(allClusters, func(item *entity.Cluster, _ int) bool {
			return item.DeployType == entity.DeployTypeDedicated
		}), func(item *entity.Cluster, _ int) uint64 {
			return item.ClusterId
		})

		nextgenIds := lo.Map(lo.Filter(allClusters, func(item *entity.Cluster, _ int) bool {
			return item.DeployType == entity.DeployTypeNextGen
		}), func(item *entity.Cluster, _ int) uint64 {
			return item.ClusterId
		})

		// list serverless clusters
		var serverlessResp *openapiserverlessv1beta1.ListClustersResponse
		if len(serverlessIds) > 0 {
			tilog.Info(ctx, "listing serverless clusters", zap.Any("ids", serverlessIds))
			serverlessResp, err = s.openAPIServerlessClusterServer.ListClusters(
				ctx,
				&openapiserverlessv1beta1.ListClustersRequest{
					Filter: fmt.Sprintf("clusterId = %q",
						strings.Join(lo.Map(serverlessIds, func(id uint64, _ int) string {
							return strconv.FormatUint(id, 10)
						}), ","),
					),
					PageSize: int32(len(serverlessIds)),
				},
			)

			if err != nil {
				return nil, err
			}
		}

		// list dedicated clusters
		var dedicatedResp *openapidedicatedv1beta1.ListClustersResponse
		if len(dedicatedIds) > 0 {
			tilog.Info(ctx, "listing dedicated clusters", zap.Any("ids", dedicatedIds))
			dedicatedResp, err = s.openAPIDedicatedClusterServer.ListClusters(
				ctx,
				&openapidedicatedv1beta1.ListClustersRequest{
					ClusterIds: lo.Map(dedicatedIds, func(id uint64, _ int) string {
						return strconv.FormatUint(id, 10)
					}),
					PageSize:       int32(len(dedicatedIds)),
					GlobalMetaOnly: request.GlobalMetaOnly,
				},
			)
			if err != nil {
				return nil, err
			}
		}

		// list nextgen clusters
		var nextgenResp *openapingv1beta2.ListTidbsResponse
		if len(nextgenIds) > 0 {
			tilog.Info(ctx, "listing nextgen clusters", zap.Any("ids", nextgenIds))
			nextgenResp, err = s.openAPINextgenClusterServer.ListTidbs(
				ctx,
				&openapingv1beta2.ListTidbsRequest{
					TidbIds: lo.Map(nextgenIds, func(id uint64, _ int) string {
						return strconv.FormatUint(id, 10)
					}),
					PageSize: int32(len(nextgenIds)),
				},
			)
			if err != nil {
				return nil, err
			}
		}

		clusterMap := lo.SliceToMap(allClusters, func(item *entity.Cluster) (string, *entity.Cluster) {
			return strconv.FormatUint(item.ClusterId, 10), item
		})

		tidbs = make([]*bffv1beta2.Tidb, 0, len(allClusters))

		// append all serverless instances
		for _, instance := range serverlessResp.GetClusters() {
			tidb, err := s.cvtTiDB(ctx, clusterMap[instance.ClusterId], instance)
			if err != nil {
				tilog.Error(ctx, "failed to convert serverless cluster to tidb", zap.Error(err))
				continue
			}
			tidbs = append(tidbs, tidb)
		}

		// append all dedicated clusters
		for _, cluster := range dedicatedResp.GetClusters() {
			tidb, err := s.cvtTiDB(ctx, clusterMap[cluster.ClusterId], cluster)
			if err != nil {
				tilog.Error(ctx, "failed to convert dedicated cluster to tidb", zap.Error(err))
				continue
			}
			tidbs = append(tidbs, tidb)
		}

		// append all nextgen instances
		for _, instance := range nextgenResp.GetTidbs() {
			tidb, err := s.cvtTiDB(ctx, clusterMap[instance.TidbId], instance)
			if err != nil {
				tilog.Error(ctx, "failed to convert nextgen cluster to tidb", zap.Error(err))
				continue
			}
			tidbs = append(tidbs, tidb)
		}

		// sort by create time desc
		slices.SortFunc(tidbs, func(a, b *bffv1beta2.Tidb) int {
			ca, cb := getCreateAt(a), getCreateAt(b)
			if ca.After(cb) {
				return -1
			}
			if ca.Before(cb) {
				return 1
			}
			return 0
		})
	}

	resp = &bffv1beta2.ListTidbsResponse{
		Tidbs:     tidbs,
		TotalSize: int32(count),
	}

	if pageToken.Offset+int64(pageSize) < count {
		pageToken.Offset += int64(pageSize)
		resp.NextPageToken = pageToken.String()
	}

	return resp, nil
}

func normalizeEngineType(engineType openapicommonv1beta1.EngineType) openapicommonv1beta1.EngineType {
	if engineType == openapicommonv1beta1.EngineType_ENGINE_TYPE_UNSPECIFIED {
		return openapicommonv1beta1.EngineType_MYSQL_COMPATIBLE
	}
	return engineType
}

func getCreateAt(c *bffv1beta2.Tidb) time.Time {
	switch c.GetServicePlan() {
	case openapicommonv1beta1.ServicePlan_Dedicated:
		return c.GetDedicatedCluster().GetCreateTime().AsTime()
	case openapicommonv1beta1.ServicePlan_Starter:
		return c.GetStarterInstance().GetCreateTime().AsTime()
	case openapicommonv1beta1.ServicePlan_Essential:
		return c.GetEssentialInstance().GetCreateTime().AsTime()
	case openapicommonv1beta1.ServicePlan_Premium:
		return c.GetPremiumInstance().GetCreateTime().AsTime()
	case openapicommonv1beta1.ServicePlan_Premium_Reserved:
		return c.GetPremiumReservedInstance().GetCreateTime().AsTime()
	case openapicommonv1beta1.ServicePlan_Essential_V2:
		return c.GetEssentialV2Instance().GetCreateTime().AsTime()
	case openapicommonv1beta1.ServicePlan_BYOC:
		return c.GetByocInstance().GetCreateTime().AsTime()
	}

	return time.Now()
}

func normalizeEngineTypesFilter(engineTypes []openapicommonv1beta1.EngineType) []string {
	return lo.FilterMap(engineTypes, func(item openapicommonv1beta1.EngineType, _ int) (string, bool) {
		if item == openapicommonv1beta1.EngineType_ENGINE_TYPE_UNSPECIFIED {
			return "", false
		}
		return item.String(), true
	})
}
