package action

import (
	"context"
	"strconv"
	"strings"

	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"
	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/regionalserver"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/entity"
	clusterrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/cluster"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	resourcepoolrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/resourcepool"
	tidbgrouprepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/tidbgroup"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/resourcepoolpolicy"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/workflow/common"
	"github.com/tidbcloud/tidb-management-service/utilities/bizerr"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

const (
	UpdatePrivateLb = "update_private_lb"
)

type UpdatePrivateLbAction struct {
	clusterRepo       clusterrepo.Repo
	tidbGroupRepo     tidbgrouprepo.Repo
	regionRepo        regionrepo.Repo
	regionalServerCli regionalserver.Client
	resourcePoolRepo  resourcepoolrepo.Repo
}

func NewUpdatePrivateLbAction(
	clusterRepo clusterrepo.Repo,
	tidbGroupRepo tidbgrouprepo.Repo,
	regionRepo regionrepo.Repo,
	regionalServerCli regionalserver.Client,
	resourcePoolRepo resourcepoolrepo.Repo,
) *UpdatePrivateLbAction {
	return &UpdatePrivateLbAction{
		clusterRepo:       clusterRepo,
		tidbGroupRepo:     tidbGroupRepo,
		regionRepo:        regionRepo,
		regionalServerCli: regionalServerCli,
		resourcePoolRepo:  resourcePoolRepo,
	}
}

func (c *UpdatePrivateLbAction) Name() string {
	return UpdatePrivateLb
}

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

func (c *UpdatePrivateLbAction) 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
	}

	tidbGroupIdsStr, ok := ctx.GetVar(common.KeyTidbGroupIds)
	if !ok {
		tilog.Error(cctx, "not found the tidbgroup ids", zap.String("tidb_group_ids", tidbGroupIdsStr))
		return bizerr.New(bizerr.ErrCodeRecordNotFound, "not found the tidbgroup ids")
	}

	enable, ok := ctx.GetVar(common.KeyPrivateLbEnable)
	if !ok {
		tilog.Error(cctx, "failed to get enable", zap.Error(err))
		return err
	}

	tilog.Info(
		cctx,
		"start to update private lb",
		zap.Uint64("cluster_id", clusterId),
		zap.String("tidb_group_ids", tidbGroupIdsStr),
		zap.String("enable", enable),
	)
	defer func() {
		tilog.Info(
			cctx,
			"end to update private lb",
			zap.Uint64("cluster_id", clusterId),
			zap.String("tidb_group_ids", tidbGroupIdsStr),
			zap.String("enable", enable),
		)
		span.End()
	}()
	if tidbGroupIdsStr == "" {
		tilog.Warn(cctx, "no tidbgroup ids to update")
		return nil
	}

	enableBool, err := strconv.ParseBool(enable)
	if err != nil {
		tilog.Error(cctx, "failed to parse enable", zap.Error(err))
		return err
	}

	cluster, err := c.clusterRepo.Get(cctx, clusterId, true)
	if err != nil {
		tilog.Error(cctx, "failed to get cluster", zap.Error(err))
		return err
	}

	if cluster.GetDeleteTime() != nil {
		tilog.Warn(
			cctx,
			"cancel the update of the private load balancer because the cluster has been deleted",
			zap.Uint64("cluster_id", clusterId),
			zap.String("tidb_group_ids", tidbGroupIdsStr),
			zap.String("enable", enable),
		)
		return nil
	}

	// not need check cluster state, because we have checked in poll tidbgroup modifiable action
	for tidbGroupId := range strings.SplitSeq(tidbGroupIdsStr, ",") {
		tidbGroupIdUint, err := strconv.ParseUint(tidbGroupId, 10, 64)
		if err != nil {
			tilog.Error(cctx, "failed to parse tidb group id", zap.Error(err))
			return err
		}
		tidbgroup, err := c.tidbGroupRepo.Load(cctx, clusterId, tidbGroupIdUint)
		if err != nil {
			tilog.Error(cctx, "failed to load tidb group", zap.Error(err))
			return err
		}

		if err := c.update(cctx, cluster, tidbgroup, enableBool); err != nil {
			tilog.Error(
				cctx,
				"failed to update private load balancer",
				zap.Error(err),
				zap.Uint64("tidb_group_id", tidbGroupIdUint),
			)
			return err
		}
	}
	return nil
}

func (c *UpdatePrivateLbAction) update(
	ctx context.Context,
	cluster *entity.Cluster,
	tidbGroupEn *entity.TidbGroup,
	enable bool,
) error {
	region, err := c.regionRepo.Get(ctx, cluster.RegionId)
	if err != nil {
		tilog.Error(ctx, "failed to find region", zap.Error(err))
		return err
	}

	getTidbGroupRequest := &clusterngv1beta.GetTiDBGroupRequest{
		ClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
		TidbGroupId: strconv.FormatUint(tidbGroupEn.Id, 10),
	}
	tidbGroup, err := c.regionalServerCli.GetTidbGroup(ctx, region, getTidbGroupRequest)
	if err != nil {
		tilog.Error(ctx, "failed to get tidb group", zap.Error(err))
		return err
	}
	if tidbGroup == nil {
		tilog.Error(ctx, "failed to get tidb group", zap.Error(err))
		return err
	}

	if tidbGroup.Phase != clusterngv1beta.TiDBGroup_MODIFYING && tidbGroup.Phase != clusterngv1beta.TiDBGroup_AVAILABLE {
		tilog.Error(ctx, "regional tidbgroup is not created", zap.Uint64("tidb_group_id", tidbGroupEn.Id))
		return bizerr.New(bizerr.ErrCodeParams, "regional tidbgroup is not created")
	}

	sqlEndpoint := buildPrivateLbSqlEndpoint(cluster, tidbGroup, enable)
	operation := resourcepoolpolicy.OperationUpdateNetwork
	if cluster.GetState() == globalngv1.Cluster_CREATING {
		operation = resourcepoolpolicy.OperationCreateInstance
	}
	stop, guardErr := guardClusterResourcePoolMutation(ctx, c.clusterRepo, c.resourcePoolRepo, cluster, operation)
	if guardErr != nil {
		return guardErr
	}
	if stop {
		return nil
	}
	if _, err := c.regionalServerCli.UpdateTidbGroup(ctx, region, &clusterngv1beta.UpdateTiDBGroupRequest{
		ClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
		TidbGroupId: strconv.FormatUint(tidbGroupEn.Id, 10),
		SqlEndpoint: sqlEndpoint,
	}); err != nil {
		tilog.Error(ctx, "failed to update tidb group", zap.Error(err))
		return err
	}

	return nil
}

func buildPrivateLbSqlEndpoint(
	cluster *entity.Cluster,
	tidbGroup *clusterngv1beta.TiDBGroup,
	enable bool,
) *clusterngv1beta.TiDBGroup_SqlEndpoint {
	if cluster.GetServicePlan() == globalcommonv1.ServicePlan_BYOC {
		// For BYOC, private LB CIDR is managed by the VPC peering allowlist
		// flow. Only toggle Enabled here and preserve any existing
		// host/port/allowlist.
		sqlEndpoint := tidbGroup.GetSqlEndpoint()
		if sqlEndpoint == nil {
			sqlEndpoint = &clusterngv1beta.TiDBGroup_SqlEndpoint{}
		}
		privateLb := sqlEndpoint.GetPrivateLb()
		if privateLb == nil {
			privateLb = &clusterngv1beta.TiDBGroup_SqlEndpoint_LoadBalancer{
				Port: 4000,
			}
		}
		if privateLb.GetPort() == 0 {
			privateLb.Port = 4000
		}
		privateLb.Enabled = enable
		sqlEndpoint.PrivateLb = privateLb
		return sqlEndpoint
	}

	// Preserve existing non-BYOC behavior: toggling private LB also resets the
	// allowlist to allow-all, which is what the current PrivateLink workflow
	// expects.
	return &clusterngv1beta.TiDBGroup_SqlEndpoint{
		PrivateLb: &clusterngv1beta.TiDBGroup_SqlEndpoint_LoadBalancer{
			Enabled: enable,
			Port:    4000,
			IpAllowList: []*clusterngv1beta.TiDBGroup_SqlEndpoint_LoadBalancer_IpAddress{
				{
					CidrBlock: "0.0.0.0/0",
				},
			},
		},
	}
}
