package cluster

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

	"github.com/samber/lo"
	"github.com/tidbcloud/tiinf/sdk/auditlog"
	"gorm.io/gorm"

	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/adapter/eventbus"
	"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"
)

const (
	maxBatchSize = 500
)

func (c serverImpl) BatchCreateClusters(
	ctx context.Context,
	req *openapiserverlessv1beta1.BatchCreateClustersRequest,
) (resp *openapiserverlessv1beta1.BatchCreateClustersResponse, err error) {
	tenantID := unknownTenantID
	startTime := time.Now()
	defer func() {
		recordWriteClusterAPIMetrics(apiBatchCreateClusters, tenantID, 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")
	}
	tenantID = strconv.FormatUint(reqMeta.OrgID, 10)

	openApiClusters := make([]*openapiserverlessv1beta1.Cluster, 0, len(req.Requests))

	// Register audit log callback.
	detailsFunc := func() auditlog.Details {
		details := eventbus.NewAuditLogDetails(eventbus.TierTypeServerless)
		details[auditlog.KeyTargetClusterId] = strings.Join(
			lo.Map(openApiClusters, func(item *openapiserverlessv1beta1.Cluster, _ int) string {
				return item.ClusterId
			}),
			",",
		)
		return details
	}
	defer func() {
		c.auditLogSender.SubmitAuditLog(ctx, auditlog.EventCreateCluster, reqMeta, detailsFunc, err)
	}()

	if len(req.Requests) > maxBatchSize {
		return nil, bizerr.New(bizerr.ErrCodeParams, "batch create clusters count exceeds the limit: %d", maxBatchSize)
	}

	clusters := make([]*entity.Cluster, 0, len(req.Requests))

	// Build clusters entities.
	for _, req := range req.Requests {
		// Get the project id.
		projectId := reqMeta.DefaultProjectID

		if req.GetCluster().Labels != nil {
			projectIdStr, ok := req.GetCluster().Labels[common.ProjectIDLabelKey]
			if ok {
				projectId, err = strconv.ParseUint(projectIdStr, 10, 64)
				if err != nil {
					return nil, bizerr.WrapErrorWithCode(
						err,
						bizerr.ErrCodeParams,
						"invalid project id: projectId=%s",
						projectIdStr,
					)
				}
			}
		}

		// Validate the project
		if err := reqMeta.CheckProjectIDsPermission(projectId); err != nil {
			return nil, bizerr.WrapErrorWithCode(
				err,
				bizerr.ErrCodePermission,
				"project id %d is not allowed, please check and try again",
				projectId,
			)
		}

		// Parse the region field.
		if req.GetCluster().Region == nil {
			return nil, bizerr.New(bizerr.ErrCodeParams, "region is invalid")
		}
		provider, region, err := common.ParseOpenApiRegionNameOrId(req.GetCluster().GetRegion().GetName())
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(
				err,
				bizerr.ErrCodeParams,
				"invalid region, please check and try again.",
			)
		}

		// Check the cluster name format
		if !common.CheckDisplayNameFmt(req.GetCluster().GetDisplayName()) {
			return nil, bizerr.New(bizerr.ErrCodeParams, "invalid display name: %s", req.GetCluster().GetDisplayName())
		}

		// Check the cluster spending limit
		if req.GetCluster().GetSpendingLimit().GetMonthly() <= 0 && req.GetCluster().GetAutoScaling().GetMaxRcu() <= 0 {
			return nil, bizerr.New(bizerr.ErrCodeParams, "not support batch create cluster with free spending limit")
		}

		cluster, err := buildCreateCluster(projectId, provider, region, reqMeta, req.GetCluster())
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "request for create cluster is invalid")
		}

		clusters = append(clusters, cluster)
	}

	// Create clusters in domain service.
	retClusters, err := c.clusterSvc.BatchCreateClusters(ctx, clusters)
	if err != nil {
		return nil, err
	}

	// Convert entities to openapi response.
	for _, cluster := range retClusters {
		oc, err := buildClusterFromEntity(cluster, nil, openapiserverlessv1beta1.ClusterView_BASIC)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeUnknown, "failed to convert cluster")
		}
		openApiClusters = append(openApiClusters, oc)
	}

	resp = &openapiserverlessv1beta1.BatchCreateClustersResponse{
		Clusters: openApiClusters,
	}
	return resp, nil
}

func (c serverImpl) BatchGetClusters(
	ctx context.Context,
	request *openapiserverlessv1beta1.BatchGetClustersRequest,
) (resp *openapiserverlessv1beta1.BatchGetClustersResponse, err error) {
	startTime := time.Now()
	defer func() {
		recordQueryClusterAPIMetrics(apiBatchGetClusters, 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")
	}

	if len(request.ClusterIds) == 0 {
		return &openapiserverlessv1beta1.BatchGetClustersResponse{
			Clusters: make([]*openapiserverlessv1beta1.Cluster, 0),
		}, nil
	}

	if len(request.ClusterIds) > maxBatchSize {
		return nil, bizerr.New(bizerr.ErrCodeParams, "too many cluster ids, max=%d", maxBatchSize)
	}

	// Create empty request.
	clusterFilter := &repository.ClusterFilter{
		DeployType:      entity.DeployTypeServerless,
		View:            entity.ClusterViewClusterViewBasic,
		ClusterIds:      make([]uint64, 0, len(request.ClusterIds)),
		OrganizationIds: []uint64{reqMeta.OrgID},
		ProjectIds:      reqMeta.ValidProjectList,
		QueryDeleted:    false,
	}
	/*
		// due to performance bottleneck, disable full view mode for batch api
		if request.GetView() == openapiserverlessv1beta1.ClusterView_FULL {
			clusterFilter.View = entity.ClusterViewClusterViewFull
		}
	*/

	for _, id := range request.ClusterIds {
		clusterId, err := strconv.ParseUint(id, 10, 64)
		if err != nil {
			return nil, bizerr.WrapErrorWithCode(err, bizerr.ErrCodeParams, "invalid cluster id: %s", id)
		}
		clusterFilter.ClusterIds = append(clusterFilter.ClusterIds, clusterId)
	}

	clusters, err := c.clusterSvc.ListClusters(ctx, clusterFilter, &common.Page{PageSize: maxBatchSize, Offset: 0})
	if err != nil {
		return nil, err
	}

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

	enrichedClusters := make([]*openapiserverlessv1beta1.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 {
		enrichedCluster, err := buildClusterFromEntity(
			cluster,
			statusMap[cluster.ClusterId],
			openapiserverlessv1beta1.ClusterView_BASIC,
		)
		if err != nil {
			return nil, bizerror.NewInternal(err, "failed to get cluster")
		}
		enrichedClusters = append(enrichedClusters, enrichedCluster)
	}

	resp = &openapiserverlessv1beta1.BatchGetClustersResponse{
		Clusters: enrichedClusters,
	}
	return resp, nil
}
