package publicconnectionsetting

import (
	"context"
	"net"
	"strconv"
	"strings"

	"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/entity"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/repository/tidbgroup"
	"github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/resourcepoolpolicy"
	servicecommon "github.com/tidbcloud/tidb-management-service/nextgen-global-service/domain/service/common"
	"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"
)

func (s *serviceImpl) Update(
	ctx context.Context,
	req *globalngv1.UpdatePublicConnectionSettingRequest,
) (*entity.PublicConnectionSetting, bizerr.BizError) {
	ctx, span := tracer.Start(ctx, "PublicConnectionSettingService.Update")
	defer span.End()

	pcs := req.GetPublicConnectionSetting()
	if pcs == nil {
		return nil, bizerr.New(bizerr.ErrCodeParams, "public connection setting is required")
	}
	if pcs.GetClearIpAllowList() && len(pcs.GetIpAllowList()) > 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "ip allow list must be empty when clear_ip_allow_list is true")
	}
	if pcs.GetClusterId() == 0 {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster_id is required")
	}

	cluster, err := s.clusterRepo.Get(ctx, pcs.GetClusterId(), false)
	if err != nil {
		return nil, err
	}

	if cluster.State != globalngv1.Cluster_AVAILABLE {
		return nil, bizerr.New(bizerr.ErrCodeParams, "cluster is not available")
	}
	resourcePoolID, parseErr := strconv.ParseUint(cluster.GetSharedPoolId(), 10, 64)
	if parseErr != nil || resourcePoolID == 0 {
		return nil, bizerr.New(bizerr.ErrCodeUnknown, "cluster has an invalid resource pool id")
	}
	resourcePool, err := s.resourcePoolRepo.Load(ctx, resourcePoolID, false)
	if err != nil {
		return nil, err
	}
	if lifecycleErr := servicecommon.RequireResourcePoolLifecycle(
		resourcePool,
		resourcepoolpolicy.OperationUpdateNetwork,
	); lifecycleErr != nil {
		return nil, lifecycleErr
	}

	// check ip allow list
	for _, ip := range pcs.GetIpAllowList() {
		if ip.GetCidrBlock() == "" {
			return nil, bizerr.New(bizerr.ErrCodeParams, "ip allow list cidr block is required")
		}
		_, _, err := net.ParseCIDR(ip.GetCidrBlock())
		if err != nil {
			return nil, bizerr.New(bizerr.ErrCodeParams, "ip allow list cidr block %s is invalid", ip.GetCidrBlock())
		}
	}

	var output *entity.PublicConnectionSetting

	if err := s.transaction.Transaction(ctx, func(ctx context.Context) error {
		lockedResourcePool, lockErr := s.resourcePoolRepo.LoadForUpdate(ctx, resourcePoolID, false)
		if lockErr != nil {
			return lockErr
		}
		if lifecycleErr := servicecommon.RequireResourcePoolLifecycle(
			lockedResourcePool,
			resourcepoolpolicy.OperationUpdateNetwork,
		); lifecycleErr != nil {
			return lifecycleErr
		}
		ev, err := entity.NewClusterEvent(ctx, cluster, clusterevent.EventTypeUpdatingPublicEndpoint, "CLUSTER_STATUS_MODIFYING")
		if err != nil {
			return err
		}
		if err := s.eventCommitter.Write(ctx, clusterevent.TopicClusterEvent, ev); err != nil {
			return err
		}

		tidbgroups, err := s.tidbGroupRepo.Query(ctx, &tidbgroup.Filter{
			ClusterIds: []uint64{pcs.GetClusterId()},
		})
		if err != nil {
			return err
		}
		tidbgroupIds := []string{}
		for _, tidbgroup := range tidbgroups {
			existingSetting, err := s.publicConnectionSettingRepo.GetByTidbGroupId(ctx, tidbgroup.Id)
			if err != nil && err.ErrCode() != bizerr.ErrCodeRecordNotFound {
				tilog.Error(ctx, "failed to get public connection existing setting by tidb group id", zap.Error(err))
				return err
			}

			setting := &entity.PublicConnectionSetting{
				PublicConnectionSetting: &globalngv1.PublicConnectionSetting{
					ClusterId:        pcs.GetClusterId(),
					TidbGroupId:      tidbgroup.Id,
					Enable:           pcs.Enable,
					IpAllowList:      pcs.GetIpAllowList(),
					ClearIpAllowList: pcs.ClearIpAllowList,
				},
			}

			// if existing setting is not nil
			// we should use its enable value if the request's enable is nil
			// and use its ip allow list if the request's ip allow list is nil
			if existingSetting != nil {
				if setting.Enable == nil {
					setting.Enable = existingSetting.Enable
				}
				if setting.GetClearIpAllowList() {
					setting.IpAllowList = make([]*globalngv1.Endpoint_IpAddress, 0)
				} else if setting.IpAllowList == nil {
					setting.IpAllowList = existingSetting.IpAllowList
				}
			} else if setting.GetClearIpAllowList() {
				setting.IpAllowList = make([]*globalngv1.Endpoint_IpAddress, 0)
			}

			if err := s.publicConnectionSettingRepo.Save(ctx, setting); err != nil {
				return err
			}
			tidbgroupIds = append(tidbgroupIds, strconv.FormatUint(tidbgroup.Id, 10))
			output = setting
		}

		workFlowErr := s.workflow.RunDagWithBizId(
			ctx,
			common.UpdatePublicLb,
			strconv.FormatUint(cluster.OrganizationId, 10),
			"0",
			strconv.FormatUint(cluster.ClusterId, 10),
			map[string]string{
				workflow.KeyTraceId:    workflow.GetTraceId(ctx),
				common.KeyClusterId:    strconv.FormatUint(cluster.ClusterId, 10),
				common.KeyTidbGroupIds: strings.Join(tidbgroupIds, ","),
			},
		)
		if workFlowErr != nil {
			return workFlowErr
		}
		return nil
	}); err != nil {
		return nil, bizerr.WrapError(err, "failed to update public connection setting")
	}

	if err := s.fullEgressIPs(ctx, cluster, output); err != nil {
		return nil, err
	}

	return output, nil
}
