package action

import (
	"context"
	"fmt"
	"strconv"

	clusterngv1beta "github.com/tidbcloud/cluster-service-ng/api/cluster-service-ng/v1beta"
	"github.com/tidbcloud/fastflow/pkg/entity/run"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	clusterreop "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/encryptionkey"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/rcuquota"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	reservedcapacitytemplaterepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/reservedcapacitytemplate"
	resourcepoolrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/resourcepoolpolicy"
	topologysvc "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/topology"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

const (
	CreateTidbCluster = "create_tidb_cluster"
)

type CreateTidbClusterAction struct {
	clusterRepo          clusterreop.Repo
	regionRepo           regionrepo.Repo
	regionalserverCli    regionalserver.Client
	encryptionKeyRepo    encryptionkey.Repo
	rcuQuotaRepo         rcuquota.Repo
	reservedTemplateRepo reservedcapacitytemplaterepo.Repo
	metadataHelper       *PremiumReservedMetadataHelper
	resourcePoolRepo     resourcepoolrepo.Repo
}

func NewCreateTidbClusterAction(
	clusterRepo clusterreop.Repo,
	regionRepo regionrepo.Repo,
	regionalserverCli regionalserver.Client,
	encryptionKeyRepo encryptionkey.Repo,
	rcuQuotaRepo rcuquota.Repo,
	reservedTemplateRepo reservedcapacitytemplaterepo.Repo,
	resourcePoolRepo resourcepoolrepo.Repo,
	metadataHelper *PremiumReservedMetadataHelper,
) *CreateTidbClusterAction {
	return &CreateTidbClusterAction{
		clusterRepo:          clusterRepo,
		regionRepo:           regionRepo,
		regionalserverCli:    regionalserverCli,
		encryptionKeyRepo:    encryptionKeyRepo,
		rcuQuotaRepo:         rcuQuotaRepo,
		reservedTemplateRepo: reservedTemplateRepo,
		metadataHelper:       metadataHelper,
		resourcePoolRepo:     resourcePoolRepo,
	}
}

func (c *CreateTidbClusterAction) Name() string {
	return CreateTidbCluster
}

func (c *CreateTidbClusterAction) ParameterNew() any {
	return &common.ClusterIdParameter{}
}

func (c *CreateTidbClusterAction) Run(ctx run.ExecuteContext, params any) error {
	cctx, span := workflow.GenerateContext(ctx)

	clusterId, err := common.GetClusterId(params)
	if err != nil {
		tilog.Error(cctx, "failed to get cluster id", zap.Error(err))
		return err
	}

	tilog.Info(
		cctx,
		"start to create tidb cluster",
		zap.Uint64("cluster_id", clusterId),
	)
	defer func() {
		tilog.Info(
			cctx,
			"end to create tidb cluster",
			zap.Uint64("cluster_id", clusterId),
		)
		span.End()
	}()

	cluster, err := c.clusterRepo.Get(cctx, clusterId, true)
	if err != nil {
		tilog.Error(cctx, "failed to get cluster", zap.Uint64("cluster_id", clusterId), zap.Error(err))
		return err
	}
	if cluster.GetDeleteTime() != nil {
		tilog.Warn(
			cctx,
			"cancel the creation of the tidb cluster because the cluster has been deleted",
			zap.Uint64("cluster_id", clusterId),
		)
		return nil
	}

	if err := c.create(cctx, cluster); err != nil {
		tilog.Error(
			cctx,
			"create tidb cluster failed",
			zap.Uint64("cluster_id", clusterId),
			zap.Error(err),
		)
		return err
	}

	return nil
}

func (c *CreateTidbClusterAction) create(
	ctx context.Context,
	cluster *entity.Cluster,
) error {
	region, err := c.regionRepo.Get(ctx, cluster.RegionId)
	if err != nil {
		tilog.Error(
			ctx,
			"failed to find region",
			zap.Uint64("cluster_id", cluster.ClusterId),
			zap.Error(err),
		)
		return err
	}

	createTidbClusterReq := &clusterngv1beta.CreateTiDBClusterRequest{
		Id:           strconv.FormatUint(cluster.ClusterId, 10),
		TenantId:     strconv.FormatUint(cluster.OrganizationId, 10),
		SharedPoolId: cluster.SharedPoolId,
		RuVersion:    cluster.RuVersion,
	}
	if cluster.GetServicePlan() == globalcommonv1.ServicePlan_Premium_Reserved {
		topologySpec, topoErr := c.buildTopologySpec(ctx, cluster)
		if topoErr != nil {
			return topoErr
		}
		createTidbClusterReq.CapacityMode = clusterngv1beta.CapacityMode_TOPOLOGY_DRIVEN
		createTidbClusterReq.CapacitySpec = &clusterngv1beta.CreateTiDBClusterRequest_TopologySpec{TopologySpec: topologySpec}
	} else {
		// calculate real max rcu with bursty ratio
		quota, err := c.rcuQuotaRepo.Load(ctx, cluster.GetOrganizationId(), cluster.ServicePlan.String())
		if err != nil {
			tilog.Error(ctx, "failed to load rcu quota",
				zap.Error(err),
				zap.Uint64("organization_id", cluster.GetOrganizationId()))
			return err
		}
		realMaxRcuWithBurstyRatio := cluster.CalculateRealMaxRcuWithBurstyRatio(ctx, quota.BurstyRatio)
		createTidbClusterReq.CapacityMode = clusterngv1beta.CapacityMode_RCU_DRIVEN
		createTidbClusterReq.CapacitySpec = &clusterngv1beta.CreateTiDBClusterRequest_RequestUnit{
			RequestUnit: &clusterngv1beta.Cluster_RequestUnit{
				Min: uint32(cluster.MinRcu),
				Max: realMaxRcuWithBurstyRatio,
			},
		}
	}

	// Native BR restore imports the source encryption metadata into the empty
	// target keyspace. Do not initialize independent metadata before restore.
	if shouldInitializeKeyspaceEncryption(cluster) {
		key, err := c.encryptionKeyRepo.Load(ctx, cluster.EncryptionKeyId, false)
		if err != nil {
			tilog.Error(
				ctx,
				"failed to load encryption key",
				zap.Uint64("cluster_id", cluster.ClusterId),
				zap.Uint64("encryption_key_id", cluster.EncryptionKeyId),
				zap.Error(err),
			)
			return err
		}
		secCfg, secErr := buildSecurityConfiguration(key)
		if secErr != nil {
			tilog.Error(ctx, "failed to build security configuration",
				zap.Uint64("cluster_id", cluster.ClusterId),
				zap.Error(secErr))
			return secErr
		}
		createTidbClusterReq.SecurityConfiguration = secCfg
	}

	stop, guardErr := guardClusterResourcePoolMutation(
		ctx,
		c.clusterRepo,
		c.resourcePoolRepo,
		cluster,
		resourcepoolpolicy.OperationCreateInstance,
	)
	if guardErr != nil {
		return guardErr
	}
	if stop {
		return nil
	}
	if _, err := c.regionalserverCli.CreateTidbCluster(ctx, region, createTidbClusterReq); err != nil {
		tilog.Error(
			ctx,
			"failed to create tidb cluster",
			zap.Uint64("cluster_id", cluster.ClusterId),
			zap.Error(err),
		)
		return err
	}
	if berr := c.metadataHelper.writeCreateMetadata(ctx, cluster); berr != nil {
		tilog.Error(ctx, "failed to write premium reserved metadata after create tidb cluster",
			zap.Uint64("cluster_id", cluster.ClusterId),
			zap.String("shared_pool_id", cluster.SharedPoolId),
			zap.Error(berr),
		)
		return berr
	}

	return nil
}

func shouldInitializeKeyspaceEncryption(cluster *entity.Cluster) bool {
	return cluster != nil && cluster.EncryptionKeyId != 0 &&
		(cluster.Cluster == nil || cluster.SourceClusterId == 0)
}

func (c *CreateTidbClusterAction) buildTopologySpec(ctx context.Context, cluster *entity.Cluster) (*clusterngv1beta.TopologySpec, error) {
	template := cluster.GetReservedCapacityTemplate()
	if template == nil {
		loadedTemplate, err := c.reservedTemplateRepo.Load(ctx, cluster.GetReservedCapacityTemplateId(), false)
		if err != nil {
			tilog.Error(ctx,
				"failed to load reserved capacity template for cluster topology",
				zap.Uint64("cluster_id", cluster.GetClusterId()),
				zap.Uint64("reserved_capacity_template_id", cluster.GetReservedCapacityTemplateId()),
				zap.Error(err),
			)
			return nil, err
		}
		template = loadedTemplate
	}

	return topologysvc.BuildTopologySpec(template.GetFixedTopology(), topologysvc.TiDBClusterComponentFilter)
}

// buildSecurityConfiguration creates the appropriate SecurityConfiguration
// based on the cloud provider of the encryption key.
func buildSecurityConfiguration(key *entity.EncryptionKey) (*clusterngv1beta.Cluster_SecurityConfiguration, error) {
	switch key.ProviderName {
	case globalcommonv1.CloudProvider_AWS:
		sc := &clusterngv1beta.Cluster_SecurityConfiguration{
			Cmek: &clusterngv1beta.Cluster_SecurityConfiguration_AwsMasterKeyArn{
				AwsMasterKeyArn: key.MasterKey,
			},
		}
		if key.CustomerManaged {
			// ExternalId pairs with the customer's AssumeRole policy condition;
			// only meaningful for customer-managed AWS KMS keys.
			sc.EncryptionContext = map[string]string{
				"ExternalId": strconv.FormatUint(key.TenantId, 10),
			}
		}
		return sc, nil
	case globalcommonv1.CloudProvider_ALICLOUD:
		return &clusterngv1beta.Cluster_SecurityConfiguration{
			Cmek: &clusterngv1beta.Cluster_SecurityConfiguration_AlicloudKmsKeyArn{
				AlicloudKmsKeyArn: key.MasterKey,
			},
		}, nil
	default:
		return nil, fmt.Errorf("unsupported cloud provider for encryption: %s", key.ProviderName.String())
	}
}
