package tidbgroup

import (
	"context"
	"slices"
	"strconv"

	"github.com/samber/lo"
	flowentity "github.com/tidbcloud/fastflow/pkg/entity"
	clusterdedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"

	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"
	"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"
)

const (
	defaultPublicTrafficFilterQuota = 100
	publicTrafficFilterQuotaErrMsg  = "Unable to add %d IP(s). This would exceed the maximum limit of %d IP addresses and cause network connection failures.\nRecommended Action: Remove unused IP addresses before adding new ones."
)

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

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

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

	err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		var err bizerr.BizError
		tidbGroup, err = s.tidbGroupRepo.Load(
			ctx,
			req.GetTidbNodeGroup().GetClusterId(),
			req.GetTidbNodeGroup().GetId(),
		)
		if err != nil {
			return err
		}

		cluster, err = s.clusterRepo.GetAndLoadComponents(ctx, tidbGroup.ClusterId)
		if err != nil {
			return err
		}

		region, err := s.regionRepo.SelectRegionById(ctx, cluster.GetRegionId(), cluster.GetOrganizationId())
		if err != nil {
			return err
		}

		var ok bool
		oldComp, ok = lo.Find(
			cluster.GetClusterComponents(),
			func(comp *globaldedicatedv1.Cluster_ClusterComponent) bool {
				return comp.GetTidbGroupId() == tidbGroup.Id &&
					comp.ComponentType == globaldedicatedv1.ComponentType_TIDB
			},
		)
		if !ok {
			return bizerr.New(bizerr.ErrCodeRecordNotFound, "tidb node group not found")
		}

		if err := s.updateCheck(ctx, region, req, cluster, tidbGroup); err != nil {
			return err
		}

		// update display name if necessary
		if req.GetTidbNodeGroup().GetDisplayName() != "" &&
			req.GetTidbNodeGroup().GetDisplayName() != tidbGroup.DisplayName {
			tidbGroup.DisplayName = req.GetTidbNodeGroup().GetDisplayName()
			if !req.GetValidateOnly() {
				if err := s.tidbGroupRepo.Save(ctx, tidbGroup); err != nil {
					return err
				}
			}
		}

		// update public endpoint if necessary
		if edp := req.GetTidbNodeGroup().GetEndpoint().GetPublicEndpoint(); edp != nil {
			var previousDisable bool

			// Only if edp.EnablePublicLb not provided, we will load previousDisable from existing record.
			if edp.EnablePublicLb == nil {
				tf, err := s.trafficFilterRepo.LoadByTidbGroupId(ctx, tidbGroup.Id, cluster.ClusterId)
				if err == nil {
					// First try to load from traffic filter repo.
					previousDisable = tf.DisablePublicLb
				} else {
					// If not found, try to load from region server.
					regionGroup, err := s.regionServerCli.GetTidbGroup(ctx, region, &clusterdedicatedv1.GetTidbGroupRequest{
						ClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
						TidbGroupId: strconv.FormatUint(tidbGroup.Id, 10),
					})
					if err != nil {
						return err
					}
					previousDisable = !regionGroup.EnablePublicLb
				}
			}

			trafficFilter = &entity.TrafficFilter{
				TidbGroupID:     tidbGroup.Id,
				ClusterID:       cluster.ClusterId,
				DisablePublicLb: lo.Ternary(edp.EnablePublicLb == nil, previousDisable, !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
				}

				// Start async reconciliation workflow.
				workFlowErr := s.workflow.RunDagWithBizId(
					ctx,
					common.UpdatePublicLb,
					strconv.FormatUint(cluster.OrganizationId, 10),
					strconv.FormatUint(cluster.ProjectId, 10),
					strconv.FormatUint(cluster.ClusterId, 10),
					map[string]string{
						workflow.KeyTraceId:   workflow.GetTraceId(ctx),
						common.KeyClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
						common.KeyTidbGroupId: strconv.FormatUint(tidbGroup.Id, 10),
					},
				)
				if workFlowErr != nil {
					return bizerr.WrapErrorWithCode(workFlowErr, bizerr.ErrCodeUnknown, "create async workflow failed")
				}
			}
		}

		// update tidb node quantity if necessary
		newComps, err = s.scaleCluster(ctx, cluster, req.GetTidbNodeGroup(), tidbGroup.Id, req.GetValidateOnly())
		if err != nil {
			return err
		}

		return nil
	})
	if err != nil {
		return nil, bizerr.WrapError(err, "failed to update work pool")
	}

	tidbComp, ok := lo.Find(newComps, func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
		return i.GetComponentType() == globaldedicatedv1.ComponentType_TIDB && i.GetTidbGroupId() == tidbGroup.Id
	})
	if !ok {
		for _, comp := range cluster.GetClusterComponents() {
			if comp.GetTidbGroupId() == tidbGroup.Id {
				tidbComp = comp
				break
			}
		}
	} else {
		// scale tidb node group
		cluster.State = globaldedicatedv1.Cluster_MODIFYING
		tidbComp.ModifyProgress = &globaldedicatedv1.Cluster_ClusterComponent_ModifyProgress{
			UptodateQuantity:          int32(oldComp.GetCount()),
			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,
						}
					},
				),
			},
		}
	}

	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.State = globaldedicatedv1.TidbNodeGroup_MODIFYING
		result.TiproxyModifyProgress = &globaldedicatedv1.Cluster_ClusterComponent_ModifyProgress{
			UptodateQuantity:          0,
			RemainingDeletionQuantity: 0,
		}
		oldTiproxyComp, ok := lo.Find(
			cluster.GetClusterComponents(),
			func(i *globaldedicatedv1.Cluster_ClusterComponent) bool {
				return i.GetComponentType() == globaldedicatedv1.ComponentType_TIPROXY &&
					i.GetTidbGroupId() == tidbGroup.Id
			},
		)
		// If the spu name is the same, we don't need to update the progress.
		if ok && oldTiproxyComp.GetSpuName() == tiproxyComp.GetSpuName() {
			result.TiproxyModifyProgress.UptodateQuantity = int32(oldTiproxyComp.GetCount())
		}
	}

	return result, nil
}

func (s *service) updateCheck(
	ctx context.Context,
	region *entity.Region,
	req *globaldedicatedv1.UpdateTidbNodeGroupRequest,
	cluster *entity.Cluster,
	tidbGroup *entity.TidbNodeGroup,
) bizerr.BizError {
	if req.GetTidbNodeGroup().GetDisplayName() != "" &&
		req.GetTidbNodeGroup().GetDisplayName() != tidbGroup.DisplayName {
		if tidbGroup.Default {
			return bizerr.New(bizerr.ErrCodeParams, "unable to update display name for default tidb group")
		}

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

		count, err := s.tidbGroupRepo.Count(ctx, &tidbgroup.Filter{
			ClusterIds:   []uint64{cluster.ClusterId},
			DisplayNames: []string{req.GetTidbNodeGroup().GetDisplayName()},
		})
		if err != nil {
			return err
		}

		if count > 0 {
			return bizerr.New(
				bizerr.ErrCodeParams,
				"display name %s already exists",
				req.GetTidbNodeGroup().GetDisplayName(),
			)
		}
	}

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

	if err := validatePublicTrafficFilterItems(req.GetTidbNodeGroup().GetEndpoint().GetPublicEndpoint()); err != nil {
		return err
	}

	if err := s.checkPublicTrafficFilterQuota(ctx, region, cluster.ClusterId, tidbGroup.Id, lo.Map(
		req.GetTidbNodeGroup().GetEndpoint().GetPublicEndpoint().GetIpAllowList(),
		func(ip *globaldedicatedv1.TidbNodeGroup_Endpoint_IpAddress, _ int) string { return ip.GetCidrBlock() },
	)); err != nil {
		return err
	}

	// Check there is no workflow running for the same tidb group.
	dagIns, err_ := s.workflow.ListRunningDagInstancesByNamePrefix(
		ctx,
		common.UpdatePublicLb,
		strconv.FormatUint(cluster.ClusterId, 10),
	)
	if err_ != nil {
		return bizerr.WrapError(err_, "failed to list running dag instances")
	}
	if slices.ContainsFunc(dagIns, func(i *flowentity.DagInstance) bool {
		return i.Vars[common.KeyTidbGroupId].Value == strconv.FormatUint(tidbGroup.Id, 10)
	}) {
		return bizerr.New(
			bizerr.ErrCodePublicEndpointRunningTask,
			"there is an operation running on this public endpoint. Please wait for it to finish and try again",
		)
	}

	// Cluster must be in normal state to update tidb group.
	if cluster.State != globaldedicatedv1.Cluster_AVAILABLE {
		return bizerr.New(
			bizerr.ErrCodePublicEndpointClusterNotReady,
			"your cluster is currently being modified. Please wait for cluster to be active and try again",
		)
	}

	return nil
}

func (s *service) checkPublicTrafficFilterQuota(
	ctx context.Context,
	region *entity.Region,
	clusterId, groupId uint64,
	inboundRules []string,
) bizerr.BizError {
	if len(inboundRules) == 0 {
		return nil
	}

	// For GCP and Azure, check against the default quota.
	cloudProvider := region.GetCloudProviderId()
	if cloudProvider == globalcommonv1.CloudProvider_GCP || cloudProvider == globalcommonv1.CloudProvider_AZURE {
		if len(inboundRules) > defaultPublicTrafficFilterQuota {
			return bizerr.New(
				bizerr.ErrCodePublicInboundRulesFailed,
				publicTrafficFilterQuotaErrMsg,
				len(inboundRules),
				defaultPublicTrafficFilterQuota,
			)
		}
		return nil
	}

	// For AWS regions, check with region server.
	if cloudProvider != globalcommonv1.CloudProvider_AWS {
		return nil
	}
	resp, err := s.regionServerCli.CheckClusterPublicLBInboundRulesStatus(
		ctx,
		region,
		&clusterdedicatedv1.CheckClusterPublicLBInboundRulesStatusRequest{
			ClusterId:    strconv.FormatUint(clusterId, 10),
			TidbGroupId:  strconv.FormatUint(groupId, 10),
			InboundRules: inboundRules,
		},
	)
	if err != nil {
		return err
	}
	if resp.GetFull() {
		return bizerr.New(bizerr.ErrCodePublicInboundRulesFailed, publicTrafficFilterQuotaErrMsg, len(inboundRules), resp.GetQuota())
	}

	return nil
}
