package tidbgroup

import (
	"context"
	"errors"
	"strconv"

	"github.com/samber/lo"

	globalcommonv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/common/v1"
	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	clusterrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cluster"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/tidbgroup"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

var (
	errDryRunMode = errors.New("dry run mode")
)

func (s *service) CreateTidbNodeGroup(
	ctx context.Context,
	req *globaldedicatedv1.CreateTidbNodeGroupRequest,
) (*globaldedicatedv1.TidbNodeGroup, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "Domain.createTidbNodeGroup")
	defer span.End()

	if req.GetTidbNodeGroup() == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "tidb_node_group is required")
	}

	if req.GetTidbNodeGroup().GetClusterId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster_id is required")
	}

	var (
		cluster       *entity.Cluster
		tidbGroup     *entity.TidbNodeGroup
		newComps      []*globaldedicatedv1.Cluster_ClusterComponent
		endpoint      *globaldedicatedv1.TidbNodeGroup_Endpoint
		trafficFilter *entity.TrafficFilter
	)

	err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		var err bizerr.BizError
		cluster, err = s.clusterRepo.GetAndLoadComponents(ctx, req.GetTidbNodeGroup().GetClusterId())
		if err != nil {
			return err
		}

		if err := s.createCheck(ctx, cluster, req.GetTidbNodeGroup()); err != nil {
			return err
		}

		tidbGroup = &entity.TidbNodeGroup{
			Id:          s.idGeneratorFacade.MustNew(),
			DisplayName: req.GetTidbNodeGroup().GetDisplayName(),
			Default:     false,
			ClusterId:   req.GetTidbNodeGroup().GetClusterId(),
		}

		// must save tidb group first
		// internal scale will use tidb group id
		if err := s.tidbGroupRepo.Save(ctx, tidbGroup); err != nil {
			return err
		}

		if edp := req.GetTidbNodeGroup().GetEndpoint().GetPublicEndpoint(); edp != nil {
			trafficFilter = &entity.TrafficFilter{
				TidbGroupID: tidbGroup.Id,
				ClusterID:   cluster.ClusterId,
				DisablePublicLb: lo.Ternary(
					edp.EnablePublicLb == nil,
					true, /* default to disable */
					!edp.GetEnablePublicLb(),
				),
				Items: lo.Map(
					edp.GetIpAllowList(),
					func(ip *globaldedicatedv1.TidbNodeGroup_Endpoint_IpAddress, _ int) *entity.TrafficFilterItem {
						return &entity.TrafficFilterItem{
							Cidr:        ip.GetCidrBlock(),
							Description: ip.GetDescription(),
						}
					},
				),
			}

			if !req.GetValidateOnly() {
				if err := s.trafficFilterRepo.Save(ctx, trafficFilter); err != nil {
					return err
				}
			}
		}

		if !req.GetValidateOnly() {
			// Start async reconciliation workflow.
			specVar := map[string]string{
				workflow.KeyTraceId:   workflow.GetTraceId(ctx),
				common.KeyClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
				common.KeyTidbGroupId: strconv.FormatUint(tidbGroup.Id, 10),
			}

			workFlowErr := s.workflow.RunDagWithBizId(
				ctx,
				common.CreateTidbGroup,
				strconv.FormatUint(cluster.OrganizationId, 10),
				strconv.FormatUint(cluster.ProjectId, 10),
				strconv.FormatUint(cluster.ClusterId, 10),
				specVar,
			)
			if workFlowErr != nil {
				return bizerr.WrapErrorWithCode(workFlowErr, bizerr.ErrCodeUnknown, "create async workflow failed")
			}
		}

		newComps, err = s.scaleCluster(ctx, cluster, req.GetTidbNodeGroup(), tidbGroup.Id, req.GetValidateOnly())
		if err != nil {
			return err
		}

		if req.GetValidateOnly() {
			return errDryRunMode
		}

		return nil
	})
	if err != nil {
		if !errors.Is(err, errDryRunMode) {
			return nil, bizerr.WrapError(err, "failed to create tidb node group")
		}
	}

	if tidbGroup == nil {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "failed to create tidb node group")
	}

	lo.ForEach(newComps, func(c *globaldedicatedv1.Cluster_ClusterComponent, _ int) {
		c.ModifyProgress = &globaldedicatedv1.Cluster_ClusterComponent_ModifyProgress{
			UptodateQuantity:          0,
			RemainingDeletionQuantity: 0,
		}
	})

	if trafficFilter != nil {
		endpoint = &globaldedicatedv1.TidbNodeGroup_Endpoint{
			PublicEndpoint: &globaldedicatedv1.TidbNodeGroup_Endpoint_PublicEndpoint{
				EnablePublicLb: lo.ToPtr(!trafficFilter.DisablePublicLb),
				IpAllowList: lo.Map(
					trafficFilter.Items,
					func(item *entity.TrafficFilterItem, _ int) *globaldedicatedv1.TidbNodeGroup_Endpoint_IpAddress {
						return &globaldedicatedv1.TidbNodeGroup_Endpoint_IpAddress{
							CidrBlock:   item.Cidr,
							Description: item.Description,
						}
					},
				),
			},
		}
	}

	tidbComp, ok := lo.Find(newComps, func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
		return i.GetComponentType() == globaldedicatedv1.ComponentType_TIDB && i.GetTidbGroupId() == tidbGroup.Id
	})
	if !ok {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "not found new tidb node group")
	}
	result := buildGlobalTidbNodeGroup(tidbGroup, tidbComp, endpoint, cluster)

	tiproxyComp, ok := lo.Find(newComps, func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
		return i.GetComponentType() == globaldedicatedv1.ComponentType_TIPROXY && i.GetTidbGroupId() == tidbGroup.Id
	})
	if ok {
		result.TiproxySetting = &globaldedicatedv1.TidbNodeGroup_TiProxySetting{
			SpuName: tiproxyComp.GetSpuName(),
			Count:   int32(tiproxyComp.GetCount()),
		}
		result.TiproxyModifyProgress = &globaldedicatedv1.Cluster_ClusterComponent_ModifyProgress{
			UptodateQuantity:          0,
			RemainingDeletionQuantity: 0,
		}
	}

	return result, nil
}

func (s *service) scaleCluster(
	ctx context.Context,
	cluster *entity.Cluster,
	tidbGroup *globaldedicatedv1.TidbNodeGroup,
	tidbGroupId uint64,
	validateOnly bool,
) ([]*globaldedicatedv1.Cluster_ClusterComponent, bizerr.BizError) {
	newComps := make([]*globaldedicatedv1.Cluster_ClusterComponent, 0)
	scaleComps := make([]*globaldedicatedv1.Cluster_ClusterComponent, 0)
	if tidbGroup.GetCount() == 0 {
		// create tidb node group
		if tidbGroup.Id == 0 {
			return nil, bizerr.New(bizerr.ErrCodeParams, "tidb node group should have at least one tidb node")
		}

		// update tidb node group, but not scale
		return newComps, nil
	}

	newTidbGroup := &globaldedicatedv1.Cluster_ClusterComponent{
		ComponentType: globaldedicatedv1.ComponentType_TIDB,
		Count:         lo.ToPtr(uint32(tidbGroup.GetCount())),
		TidbGroupId:   tidbGroupId,
	}

	// set the default tidb group tier name
	tidbDefaultGroupComp, ok := lo.Find(
		cluster.GetClusterComponents(),
		func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
			return i.GetIsDefaultTidbGroup() && i.GetComponentType() == globaldedicatedv1.ComponentType_TIDB
		},
	)
	if !ok {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "default tidb node group not found")
	}
	newTidbGroup.SpuName = tidbDefaultGroupComp.GetSpuName()
	scaleComps = append(scaleComps, newTidbGroup)

	if tidbGroup.GetTiproxySetting() != nil {
		newTiproxyGroup := &globaldedicatedv1.Cluster_ClusterComponent{
			ComponentType: globaldedicatedv1.ComponentType_TIPROXY,
			Count:         lo.ToPtr(uint32(tidbGroup.GetTiproxySetting().GetCount())),
			TidbGroupId:   tidbGroupId,
			SpuName:       tidbGroup.GetTiproxySetting().GetSpuName(),
		}
		scaleComps = append(scaleComps, newTiproxyGroup)
	}

	newCluster, err := s.clusterService.Scale(ctx, &globaldedicatedv1.ScaleClusterWithMultiComponentRequest{
		ClusterId:         tidbGroup.GetClusterId(),
		ValidateOnly:      validateOnly,
		ClusterComponents: scaleComps,
		ForceScale:        false,
	})

	if err != nil {
		return nil, err
	}

	for _, component := range newCluster.GetClusterComponents() {
		if component.GetComponentType() == globaldedicatedv1.ComponentType_TIDB &&
			component.GetTidbGroupId() == tidbGroupId {
			newComps = append(newComps, component)
		}
		if tidbGroup.GetTiproxySetting() != nil &&
			component.GetComponentType() == globaldedicatedv1.ComponentType_TIPROXY &&
			component.GetTidbGroupId() == tidbGroupId {
			newComps = append(newComps, component)
		}
	}
	if len(newComps) == 0 {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "not found new tidb node group")
	}

	return newComps, nil
}

func (s *service) createCheck(
	ctx context.Context,
	cluster *entity.Cluster,
	tidbGroup *globaldedicatedv1.TidbNodeGroup,
) bizerr.BizError {
	switch cluster.CloudProviderId {
	case globalcommonv1.CloudProvider_AWS:
		// Empty case - AWS is supported without additional checks
	case globalcommonv1.CloudProvider_GCP:
		clusters, err := s.clusterRepo.Query(ctx, &clusterrepo.Filter{
			ProjectIds:  []uint64{cluster.ProjectId},
			RegionNames: []entity.RegionName{cluster.RegionId},
		})
		if err != nil {
			return err
		}
		count, err := s.tidbGroupRepo.Count(ctx, &tidbgroup.Filter{
			ClusterIds: lo.Map(clusters, func(c *entity.Cluster, _ int) uint64 { return c.ClusterId }),
		})
		if err != nil {
			return err
		}
		if count >= int64(s.cfg.BizConfig.MaxTidbNodeGroupCountPerProjectInGCP) {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"You have reached the maximum limit of %d private endpoint services for this VPC on Google Cloud. To create a new cluster or additional TiDB node groups, consider setting up clusters in a new project",
				s.cfg.BizConfig.MaxTidbNodeGroupCountPerProjectInGCP,
			)
		}
	default:
		// Azure and other cloud providers are not supported yet
		return bizerr.New(bizerr.ErrCodeUnknown, "unsupported cloud provider %s", cluster.CloudProviderId)
	}
	filter := &tidbgroup.Filter{
		Ids:        nil,
		ClusterIds: []uint64{cluster.ClusterId},
	}
	count, err := s.tidbGroupRepo.Count(ctx, filter)
	if err != nil {
		return err
	}
	if count >= int64(s.cfg.BizConfig.MaxTidbNodeGroupCount) {
		return bizerr.New(
			bizerr.ErrCodeParams,
			"each cluster can have %d TiDB node groups at most",
			s.cfg.BizConfig.MaxTidbNodeGroupCount,
		)
	}

	if cluster.State != globaldedicatedv1.Cluster_AVAILABLE {
		return bizerr.New(bizerr.ErrCodeParams, "cluster is not available for this action")
	}

	if !genericNameRegexp.MatchString(tidbGroup.GetDisplayName()) {
		return bizerr.New(bizerr.ErrCodeParams, "display name %s", genericNameErrMsg)
	}

	tidbDefaultGroupComp, ok := lo.Find(
		cluster.GetClusterComponents(),
		func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
			return i.GetIsDefaultTidbGroup()
		},
	)
	if !ok {
		return bizerr.New(bizerr.ErrCodeUnknown, "default tidb node group not found")
	}
	if tidbGroup.GetSpuName() != "" && tidbGroup.GetSpuName() != tidbDefaultGroupComp.GetSpuName() {
		return bizerr.New(bizerr.ErrCodeParams, "creating TiDB node group with specific tier is not allowed")
	}

	if tidbGroup.GetTiproxySetting() != nil {
		if err := checkTiProxySpu(cluster, tidbGroup.GetTiproxySetting()); err != nil {
			return err
		}
	}

	// check if the display name is unique
	filter.DisplayNames = []string{tidbGroup.GetDisplayName()}
	count, err = s.tidbGroupRepo.Count(ctx, filter)
	if err != nil {
		return err
	}
	if count > 0 {
		return bizerr.New(bizerr.ErrCodeParams, "display name %s is already used", tidbGroup.GetDisplayName())
	}

	return validatePublicTrafficFilterItems(tidbGroup.GetEndpoint().GetPublicEndpoint())
}
