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"

	globalngv1 "github.com/tidbcloud/tidb-management-service/api/spec/global/nextgen/v1"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/adapter/eventbus"
	"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"
	publicconnectionsettingrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/publicconnectionsetting"
	regionrepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/region"
	tidbgrouprepo "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/tidbgroup"
	"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/clusterevent"
	"github.com/tidbcloud/tidb-management-service/utilities/workflow"
)

const (
	UpdatePublicLb = "update_public_lb"
)

type UpdatePublicLbAction struct {
	clusterRepo                 clusterrepo.Repo
	tidbGroupRepo               tidbgrouprepo.Repo
	regionRepo                  regionrepo.Repo
	publicConnectionSettingRepo publicconnectionsettingrepo.Repo
	regionalServerCli           regionalserver.Client
	eventCommitter              eventbus.WALCommitter
}

func NewUpdatePublicLbAction(
	clusterRepo clusterrepo.Repo,
	tidbGroupRepo tidbgrouprepo.Repo,
	regionRepo regionrepo.Repo,
	publicConnectionSettingRepo publicconnectionsettingrepo.Repo,
	regionalServerCli regionalserver.Client,
	eventCommitter eventbus.WALCommitter,
) *UpdatePublicLbAction {
	return &UpdatePublicLbAction{
		clusterRepo:                 clusterRepo,
		tidbGroupRepo:               tidbGroupRepo,
		regionRepo:                  regionRepo,
		publicConnectionSettingRepo: publicConnectionSettingRepo,
		regionalServerCli:           regionalServerCli,
		eventCommitter:              eventCommitter,
	}
}

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

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

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

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

	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.String("tidb_group_ids", tidbGroupIdsStr),
		)
		return nil
	}

	switch cluster.State {
	case globalngv1.Cluster_AVAILABLE, globalngv1.Cluster_MODIFYING:
		// 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)
	}

	for tidbGroupId := range strings.SplitSeq(tidbGroupIdsStr, ",") {
		tgId, 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, tgId)
		if err != nil {
			tilog.Error(cctx, "failed to load tidb group", zap.Error(err))
			return err
		}

		publicconnectionsetting, err := c.publicConnectionSettingRepo.GetByTidbGroupId(cctx, tgId)
		if err != nil {
			tilog.Error(
				cctx,
				"failed to get public connection setting",
				zap.Error(err),
				zap.Uint64("tidb_group_id", tgId),
			)
			return err
		}

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

	ev, err := entity.NewClusterEvent(cctx, cluster, clusterevent.EventTypeUpdatedPublicEndpoint, "CLUSTER_STATUS_NORMAL")
	if err != nil {
		return err
	}
	if err := c.eventCommitter.Write(cctx, clusterevent.TopicClusterEvent, ev); err != nil {
		return err
	}
	return nil
}

func (c *UpdatePublicLbAction) update(
	ctx context.Context,
	cluster *entity.Cluster,
	tidbGroupEn *entity.TidbGroup,
	publicConnectionSetting *entity.PublicConnectionSetting,
) 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 := &clusterngv1beta.TiDBGroup_SqlEndpoint{}

	sqlEndpoint.PublicLb = &clusterngv1beta.TiDBGroup_SqlEndpoint_LoadBalancer{
		Enabled:     publicConnectionSetting.GetEnable(),
		Port:        4000,
		IpAllowList: []*clusterngv1beta.TiDBGroup_SqlEndpoint_LoadBalancer_IpAddress{},
	}
	if len(publicConnectionSetting.GetIpAllowList()) > 0 {
		for _, ip := range publicConnectionSetting.GetIpAllowList() {
			sqlEndpoint.PublicLb.IpAllowList = append(
				sqlEndpoint.PublicLb.IpAllowList,
				&clusterngv1beta.TiDBGroup_SqlEndpoint_LoadBalancer_IpAddress{
					CidrBlock: ip.CidrBlock,
				},
			)
		}
	}

	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
}
