package action

import (
	"context"
	"strconv"

	"github.com/tidbcloud/fastflow/pkg/entity/run"
	clusterdedicatedv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	"github.com/tidbcloud/tiinf/monitor/tilog"
	"go.uber.org/zap"

	globaldedicatedv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/dedicated/v1"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/adapter/regionserver"
	"github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/entity"
	clusterrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/cluster"
	endpointrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/endpoint"
	regionrepo "github.com/tidbcloud/tidb-management-service/dedicated-global-service/domain/repository/region"
	"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 (
	UpdatePublicLb = "update_public_lb"
)

type UpdatePublicLbAction struct {
	clusterRepo       clusterrepo.Repo
	tidbGroupRepo     tidbgroup.Repo
	regionRepo        regionrepo.Repo
	trafficFilterRepo endpointrepo.TrafficFilterRepo
	regionServerCli   regionserver.Client
}

func NewUpdatePublicLbAction(
	clusterRepo clusterrepo.Repo,
	tidbGroupRepo tidbgroup.Repo,
	regionRepo regionrepo.Repo,
	trafficFilterRepo endpointrepo.TrafficFilterRepo,
	regionServerCli regionserver.Client,
) *UpdatePublicLbAction {
	return &UpdatePublicLbAction{
		clusterRepo:       clusterRepo,
		tidbGroupRepo:     tidbGroupRepo,
		regionRepo:        regionRepo,
		trafficFilterRepo: trafficFilterRepo,
		regionServerCli:   regionServerCli,
	}
}

func (c *UpdatePublicLbAction) Name() string {
	return UpdatePublicLb
}

func (c *UpdatePublicLbAction) ParameterNew() any {
	return &common.TidbGroupParameter{}
}

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

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

	tilog.Info(
		cctx,
		"start to update public lb",
		zap.Uint64("cluster_id", clusterId),
		zap.Uint64("tidb_group_id", tidbGroupId),
	)
	defer func() {
		tilog.Info(
			cctx,
			"end to update public lb",
			zap.Uint64("cluster_id", clusterId),
			zap.Uint64("tidb_group_id", tidbGroupId),
		)
		span.End()
	}()

	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 public load balancer because the cluster has been deleted",
			zap.Uint64("cluster_id", clusterId),
			zap.Uint64("tidb_group_id", tidbGroupId),
		)
		return nil
	}

	switch cluster.State {
	case globaldedicatedv1.Cluster_AVAILABLE,
		globaldedicatedv1.Cluster_PAUSING,
		globaldedicatedv1.Cluster_PAUSED,
		globaldedicatedv1.Cluster_SCALING:
		// do nothing
	default:
		tilog.Error(
			cctx,
			"cluster state is not ready to update public lb",
			zap.Uint64("cluster_id", clusterId),
			zap.String("state", cluster.State.String()),
		)
		return bizerr.New(bizerr.ErrCodeParams, "cluster state %s is not ready to update public lb", cluster.State)
	}

	tidbGroup, err := c.tidbGroupRepo.Load(cctx, clusterId, tidbGroupId)
	if err != nil {
		tilog.Error(
			cctx,
			"failed to load work pool",
			zap.Uint64("cluster_id", clusterId),
			zap.Uint64("tidb_group_id", tidbGroupId),
			zap.Error(err),
		)
		return err
	}

	trafficFilter, err := c.trafficFilterRepo.LoadByTidbGroupId(cctx, clusterId, tidbGroupId)
	if err != nil {
		tilog.Error(
			cctx,
			"failed to load traffic filter",
			zap.Uint64("cluster_id", clusterId),
			zap.Uint64("tidb_group_id", tidbGroupId),
			zap.Error(err),
		)
		return err
	}

	if err := c.update(cctx, cluster, tidbGroup, trafficFilter); err != nil {
		tilog.Error(
			cctx,
			"failed to update load balancer",
			zap.Uint64("cluster_id", clusterId),
			zap.Uint64("tidb_group_id", tidbGroupId),
			zap.Error(err),
		)
		return err
	}

	return nil
}

func (c *UpdatePublicLbAction) update(
	ctx context.Context,
	cluster *entity.Cluster,
	tidbGroupEn *entity.TidbNodeGroup,
	trafficFilter *entity.TrafficFilter,
) error {
	region, err := c.regionRepo.SelectRegionById(ctx, cluster.RegionId, cluster.GetOrganizationId())
	if err != nil {
		tilog.Error(ctx, "failed to find region", zap.Error(err))
		return err
	}

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

	tidbGroup.EnablePublicLb = !trafficFilter.DisablePublicLb
	var ipAllowList []*clusterdedicatedv1.LoadBalancer_IpAddress
	for _, each := range trafficFilter.Items {
		ipAllowList = append(ipAllowList, &clusterdedicatedv1.LoadBalancer_IpAddress{
			CidrBlock: each.Cidr,
		})
	}
	if len(ipAllowList) == 0 {
		ipAllowList = append(ipAllowList, &clusterdedicatedv1.LoadBalancer_IpAddress{
			CidrBlock: DefaultInternalCIDR,
		})
	}
	tidbGroup.PublicLb.IpAllowList = ipAllowList

	// Force the port to be cluster port.
	tidbGroup.PublicLb.Port = int32(cluster.GetPort())
	// There is an api bug in cluster-controller-service api.
	if tidbGroup.PrivateLb != nil {
		tidbGroup.PrivateLb.Port = int32(cluster.GetPort())
	}

	updateTidbGroupRequest := &clusterdedicatedv1.UpdateTidbGroupRequest{
		ClusterId:   strconv.FormatUint(cluster.ClusterId, 10),
		TidbGroupId: strconv.FormatUint(tidbGroupEn.Id, 10),
		TidbGroup:   tidbGroup,
	}

	if _, err := c.regionServerCli.UpdateTidbGroup(ctx, region, updateTidbGroupRequest); err != nil {
		tilog.Error(ctx, "failed to update tidb group", zap.Error(err))
		return err
	}

	return nil
}
