package shadowpool

import (
	"context"
	"fmt"
	"strconv"

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

	globalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/serverless/v1"
	regionalserverlessv1 "github.com/tidbcloud/tidb-management-service/api/spec/regional/serverless/v1"
	openapicommonv1beta1 "github.com/tidbcloud/tidb-management-service/api/spec/tidb_cloud_open_api/common/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/enum"
	"github.com/tidbcloud/tidb-management-service/utilities/grpc/metadata"
)

// Fulfil shadow pool clusters to ensure the desired capacity is met.
func (w *worker) Fulfil(ctx context.Context) error {
	tilog.Info(ctx, "Starting Fulfil operation for shadow pools")

	// List all enabled shadow pools
	pools, err := w.poolRepo.List(ctx, &repository.ShadowPoolFilter{
		OnlyEnabled: true,
	})
	if err != nil {
		tilog.Error(ctx, "Failed to list shadow pools", zap.Error(err))
		return fmt.Errorf("failed to list shadow pools: %w", err)
	}

	tilog.Info(ctx, "Found shadow pools to fulfil", zap.Int("count", len(pools)))

	// Process each pool
	for _, pool := range pools {
		if err := w.fulfilForSingleShadowPool(ctx, pool.ID); err != nil {
			tilog.Error(ctx, "Failed to fulfil shadow pool",
				zap.Uint64("pool_id", pool.ID),
				zap.String("pool_name", pool.Name),
				zap.Error(err))
			fulfilFailureTotal.WithLabelValues(
				fmt.Sprintf("%d", pool.ID),
				pool.Name,
			).Inc()
			// Continue with next pool even if one fails
			continue
		}
		fulfilSuccessTotal.WithLabelValues(
			fmt.Sprintf("%d", pool.ID),
			pool.Name,
		).Inc()
	}

	tilog.Info(ctx, "Completed Fulfil operation for shadow pools")
	return nil
}

func (w *worker) fulfilForSingleShadowPool(ctx context.Context, poolID uint64) error {
	tilog.Debug(ctx, "Fulfilling shadow pool", zap.Uint64("pool_id", poolID))

	// Get the shadow pool
	pool, err := w.poolRepo.Get(ctx, poolID)
	if err != nil {
		return fmt.Errorf("failed to get shadow pool: %w", err)
	}

	ctx = metadata.AppendToOutgoingMeta(ctx, &metadata.RequestMeta{
		UserID:            w.getFakeUserID(),
		OrgID:             pool.OrganizationID,
		DefaultProjectID:  pool.ProjectID,
		ValidProjectList:  []uint64{pool.ProjectID},
		IsCheckPermission: lo.ToPtr(true),
	})

	// Only process Starter service plan
	if pool.ServicePlan != openapicommonv1beta1.ServicePlan_Starter.String() {
		tilog.Debug(ctx, "Skipping non-Starter service plan",
			zap.Uint64("pool_id", poolID),
			zap.String("service_plan", pool.ServicePlan))
		return nil
	}

	// Query all clusters for this pool
	count, err := w.clusterRepo.CountClusterByFilter(ctx, &repository.ClusterFilter{
		QueryDeleted:  false,
		ShadowPoolIds: []uint64{pool.ID},
		ShadowStates: []entity.ShadowState{
			entity.ShadowStatePreparing,
			entity.ShadowStatePrepared,
		},
	})
	if err != nil {
		return fmt.Errorf("failed to query clusters: %w", err)
	}

	// Calculate how many clusters need to be created
	targetCount := pool.MinCapacity
	needToCreate := targetCount - int(count)

	if needToCreate <= 0 {
		tilog.Info(ctx, "Shadow pool already has enough clusters",
			zap.Uint64("pool_id", poolID),
			zap.Int64("prepared", count),
			zap.Int("min_capacity", pool.MinCapacity))
		return nil
	}

	// Apply batch size limit
	if pool.AutoScalingConfig.MaxBatchSize > 0 && needToCreate > pool.AutoScalingConfig.MaxBatchSize {
		needToCreate = pool.AutoScalingConfig.MaxBatchSize
	}

	tilog.Info(ctx, "Creating clusters for shadow pool",
		zap.Uint64("pool_id", poolID),
		zap.Int("to_create", needToCreate))

	// Track successfully created clusters
	createdCount := 0

	// Create clusters
	for i := 0; i < needToCreate; i++ {
		// Generate a unique cluster ID for this shadow pool cluster
		clusterID := entity.GenClusterID()

		// Parse cloud provider
		cloudProvider, err := parseCloudProvider(pool.CloudProvider)
		if err != nil {
			tilog.Error(ctx, "Failed to parse cloud provider",
				zap.Uint64("pool_id", poolID),
				zap.String("cloud_provider", pool.CloudProvider),
				zap.Error(err))
			continue
		}

		cluster := &entity.Cluster{
			OrganizationId: pool.OrganizationID,
			ProjectId:      pool.ProjectID,
			DisplayName:    fmt.Sprintf("shadow-%d-%d", poolID, clusterID),
			DeployType:     entity.DeployTypeServerless,
			Region:         pool.Region,
			CloudProvider:  cloudProvider,
			ServicePlan:    openapicommonv1beta1.ServicePlan_Starter,
			ShadowPoolId:   poolID,
			ShadowState:    entity.ShadowStatePreparing,
			CreatedBy:      w.getCreatedBy(),
			UserId:         w.getFakeUserID(),
			Labels: map[string]string{
				common.ProjectIDLabelKey: strconv.FormatUint(pool.ProjectID, 10),
				common.OrgIDLabelKey:     strconv.FormatUint(pool.OrganizationID, 10),
				metadata.UserIDHeaderKey: w.getFakeUserID(),
			},
		}

		globalCluster := &globalserverlessv1.ServerlessServiceCluster{
			TenantId:      pool.OrganizationID,
			ProjectId:     pool.ProjectID,
			CloudProvider: pool.CloudProvider,
			Region:        pool.Region,
			SpendLimit: &globalserverlessv1.SpendLimit{
				Monthly: uint32(pool.InstanceDemand.MonthlySpendingLimit),
				Daily:   uint32(pool.InstanceDemand.DailySpendingLimit),
			},
			BackupPolicy: nil,
			Cluster: &regionalserverlessv1.ServerlessRegionalServiceClusterInfo{
				EnhancedEncryptionEnabled: false,
				Endpoints: &regionalserverlessv1.Endpoints{
					Public: &regionalserverlessv1.PublicEndpoint{
						Disabled: false,
					},
				},
				ZoneRedundantEnabled: false,
			},
		}

		cluster.GlobalCluster = globalCluster

		// Pass InitSQLs from the shadow pool to the cluster for execution during initialization
		if len(pool.InitSQLs) > 0 {
			cluster.InitSQLs = pool.InitSQLs
		}

		// Create cluster using the CreateCluster domain service
		_, err = w.serverlessClusterSvc.CreateCluster(ctx, cluster)
		if err != nil {
			tilog.Error(ctx, "Failed to create cluster for shadow pool",
				zap.Uint64("pool_id", poolID),
				zap.Error(err))
			// Continue creating other clusters even if one fails
			continue
		}

		createdCount++
		tilog.Debug(ctx, "Created cluster for shadow pool",
			zap.Uint64("pool_id", poolID),
			zap.Uint64("cluster_id", cluster.ClusterId))
	}

	// Record the number of clusters created in this fulfill operation
	fulfilQuantity.WithLabelValues(
		fmt.Sprintf("%d", poolID),
		pool.Name,
	).Add(float64(createdCount))

	return nil
}

func parseCloudProvider(provider string) (openapicommonv1beta1.Region_CloudProvider, error) {
	cloudProvider, err := enum.FromString[openapicommonv1beta1.Region_CloudProvider](provider)
	if err != nil {
		return openapicommonv1beta1.Region_CLOUD_PROVIDER_UNSPECIFIED, fmt.Errorf("invalid cloud provider: %s", provider)
	}
	return openapicommonv1beta1.Region_CloudProvider(cloudProvider), nil
}

// Default values for shadow pool configuration when not provided or empty in config
// These values are preserved from the previous hardcoded constants for backward compatibility
const (
	// defaultFakeUserID is the default user ID used for shadow pool cluster operations
	// when not specified or empty in configuration
	defaultFakeUserID = "70249"
	// defaultCreatedBy is the default creator identifier for shadow pool clusters
	// when not specified or empty in configuration
	defaultCreatedBy = "shadow-pool-worker"
)

// getFakeUserID returns the fake user ID from config, or a default value if not configured
func (w *worker) getFakeUserID() string {
	if w.config != nil && w.config.FakeUserID != "" {
		return w.config.FakeUserID
	}
	return defaultFakeUserID
}

// getCreatedBy returns the created by value from config, or a default value if not configured
func (w *worker) getCreatedBy() string {
	if w.config != nil && w.config.CreatedBy != "" {
		return w.config.CreatedBy
	}
	return defaultCreatedBy
}
