package privatelinkservice

import (
	"context"
	"fmt"

	"github.com/pingcap/errors"
	"k8s.io/client-go/util/retry"
	"sigs.k8s.io/controller-runtime/pkg/client"

	clusterctlpb "github.com/tidbcloud/infra-provider/api/tidbcloud/cluster-server/v1"
	regionalapiv1 "github.com/tidbcloud/infra-provider/api/tidbcloud/regional-server/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/endpointservice"
	bizerr "github.com/tidbcloud/infra-provider/regional-server/pkg/common/errors"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/privatelinkgroup"
)

type privateLinkIPAddressState struct {
	status         *regionalapiv1.PrivateLinkIPAddressTypeStatus
	infrastructure privatelinkgroup.Evaluation
	normal         *normalPrivateLinkService
	plsTarget      v1alpha1.IPAddressType
	observed       v1alpha1.IPAddressType
	available      bool
	blocker        error
}

func (s *PrivateLinkServiceService) GetPrivateLinkIPAddressType(
	ctx context.Context,
	req *regionalapiv1.GetPrivateLinkIPAddressTypeRequest,
) (*regionalapiv1.PrivateLinkIPAddressTypeStatus, error) {
	if err := s.validatePrivateLinkGroupRequest(req.GetClusterId(), req.GetTidbGroupId()); err != nil {
		return nil, err
	}
	if s.groupManager == nil {
		return nil, bizerr.New(bizerr.InternalServerError, "TiDB Group PrivateLink address evaluator is not configured")
	}
	state, err := s.observePrivateLinkIPAddressState(
		ctx,
		req.GetClusterId(),
		req.GetTidbGroupId(),
		"",
	)
	if err != nil {
		return nil, err
	}
	return state.status, nil
}

// ValidatePrivateLinkIPv4Only checks whether the next IPv4 transition step can
// disable IPv6 without mutating the Group, PrivateLinkService, or AWS resources.
// SetPrivateLinkIPAddressType and the Endpoint Service controller remain the
// authoritative checks for races after this admission-time observation.
func (s *PrivateLinkServiceService) ValidatePrivateLinkIPv4Only(
	ctx context.Context,
	req *regionalapiv1.GetPrivateLinkIPAddressTypeRequest,
) error {
	if err := s.validatePrivateLinkGroupRequest(req.GetClusterId(), req.GetTidbGroupId()); err != nil {
		return err
	}
	if s.groupManager == nil {
		return bizerr.New(bizerr.InternalServerError, "TiDB Group PrivateLink address evaluator is not configured")
	}
	state, err := s.observePrivateLinkIPAddressState(
		ctx,
		req.GetClusterId(),
		req.GetTidbGroupId(),
		v1alpha1.IPAddressTypeIPv4,
	)
	if err != nil {
		return err
	}
	if state.blocker != nil {
		return state.blocker
	}
	if state.infrastructure.Requested == v1alpha1.IPAddressTypeIPv4 &&
		state.available && state.observed == v1alpha1.IPAddressTypeIPv4 {
		return nil
	}
	return s.validateObservedGroupCanDisableIPv6(ctx, state.normal, state.available)
}

func (s *PrivateLinkServiceService) observePrivateLinkIPAddressState(
	ctx context.Context,
	clusterID string,
	groupID string,
	transitionTarget v1alpha1.IPAddressType,
) (*privateLinkIPAddressState, error) {
	infrastructure, err := s.groupManager.Evaluate(ctx, clusterID, groupID)
	if err != nil {
		if errors.Cause(err) == privatelinkgroup.ErrTiDBGroupNotReady {
			// New Groups are IPv4-only until an explicit address transition.
			// Keep the public desired value valid while Cluster CR propagation lags.
			return &privateLinkIPAddressState{
				infrastructure: privatelinkgroup.Evaluation{
					Requested: v1alpha1.IPAddressTypeIPv4,
					NLBTarget: v1alpha1.IPAddressTypeIPv4,
				},
				status: &regionalapiv1.PrivateLinkIPAddressTypeStatus{
					DesiredIpAddressType: regionalapiv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_IPV4,
					Message:              err.Error(),
				},
				blocker: bizerr.New(bizerr.PrivateLinkAddressPending, err.Error()),
			}, nil
		}
		if errors.Cause(err) == privatelinkgroup.ErrPrivateNLBDisabled {
			return nil, bizerr.New(
				bizerr.PreconditionFailed,
				"the private NLB for the TiDB Group must be enabled before changing its PrivateLink IP address type",
			)
		}
		return nil, bizerr.New(
			bizerr.InternalServerError,
			"failed to observe TiDB Group PrivateLink infrastructure: "+err.Error(),
		)
	}

	state := &privateLinkIPAddressState{
		infrastructure: infrastructure,
		status: &regionalapiv1.PrivateLinkIPAddressTypeStatus{
			DesiredIpAddressType: privateLinkServiceIPAddressTypeToPB(infrastructure.Requested),
			Message:              infrastructure.Message,
		},
	}
	normal, err := s.normalGroupPrivateLinkService(
		ctx,
		clusterID,
		groupID,
		infrastructure.VPC(),
		infrastructure.PrivateNLBARN,
	)
	if err != nil {
		if bizerr.Is(err, bizerr.PrivateLinkAddressPending) ||
			bizerr.Is(err, bizerr.PrivateLinkAddressBlocked) ||
			bizerr.Is(err, bizerr.PreconditionFailed) {
			state.blocker = err
			state.status.Message = err.Error()
			return state, nil
		}
		return nil, err
	}
	state.normal = normal
	state.plsTarget = effectivePrivateLinkServiceAddressType(normal.pls.Spec.TargetIPAddressType)
	state.observed, state.available, err = s.observeEndpointServiceAddressType(ctx, normal)
	if err != nil {
		if errors.Cause(err) == endpointservice.ErrIPv6Only &&
			(transitionTarget == v1alpha1.IPAddressTypeIPv4 ||
				(transitionTarget == "" && infrastructure.Requested == v1alpha1.IPAddressTypeIPv4)) {
			return nil, bizerr.New(
				bizerr.PrivateLinkAddressBlocked,
				"cannot disable IPv6: the normal PrivateLinkService Endpoint Service is IPv6-only",
			)
		}
		if _, ok := err.(bizerr.Error); ok {
			return nil, err
		}
		return nil, bizerr.New(
			bizerr.AWSClientError,
			"failed to observe the normal PrivateLinkService Endpoint Service: "+err.Error(),
		)
	}
	if state.available {
		state.status.ObservedIpAddressType = privateLinkServiceIPAddressTypeToPB(state.observed)
	}

	if !infrastructure.NLBReady {
		state.status.Message = fmt.Sprintf(
			"%s; requested target is %s and NLB target is %s",
			infrastructure.Message,
			infrastructure.Requested,
			infrastructure.NLBTarget,
		)
		return state, nil
	}
	if infrastructure.NLBTarget != infrastructure.Requested {
		state.status.Message = fmt.Sprintf(
			"waiting for requested target %s and NLB target %s to converge",
			infrastructure.Requested,
			infrastructure.NLBTarget,
		)
		return state, nil
	}
	// Empty is a backward-compatible IPv4 target only. A dualstack request must
	// have an explicit PLS target so the EndpointService controller durably owns
	// provider-side reconciliation before the workflow can report READY.
	if state.plsTarget != infrastructure.Requested {
		if state.available {
			state.status.Message = fmt.Sprintf(
				"waiting for requested target %s, normal PrivateLinkService target %s, and observed Endpoint Service address type %s to converge",
				infrastructure.Requested,
				state.plsTarget,
				state.observed,
			)
		} else {
			state.status.Message = fmt.Sprintf(
				"waiting for requested target %s and normal PrivateLinkService target %s to converge",
				infrastructure.Requested,
				state.plsTarget,
			)
		}
		return state, nil
	}
	if !state.available {
		state.status.Message = "waiting for the normal PrivateLinkService Endpoint Service to become available"
		return state, nil
	}
	if state.observed != infrastructure.Requested {
		state.status.Message = fmt.Sprintf(
			"waiting for requested target %s and observed Endpoint Service address type %s to converge",
			infrastructure.Requested,
			state.observed,
		)
		return state, nil
	}

	value := privateLinkServiceIPAddressTypeToPB(infrastructure.Requested)
	state.status = &regionalapiv1.PrivateLinkIPAddressTypeStatus{
		DesiredIpAddressType:  value,
		ObservedIpAddressType: value,
		Ready:                 true,
		Message: fmt.Sprintf(
			"TiDB Group PrivateLink address capability is ready for %s",
			infrastructure.Requested,
		),
	}
	return state, nil
}

// SetPrivateLinkIPAddressType advances the durable Group transition.
// Upgrade changes the NLB before the PLS; downgrade changes the PLS before the NLB.
func (s *PrivateLinkServiceService) SetPrivateLinkIPAddressType(
	ctx context.Context,
	req *regionalapiv1.SetPrivateLinkIPAddressTypeRequest,
) error {
	if err := s.validatePrivateLinkGroupRequest(req.GetClusterId(), req.GetTidbGroupId()); err != nil {
		return err
	}
	target, err := privateLinkServiceIPAddressTypeFromPB(req.GetIpAddressType())
	if err != nil {
		return err
	}
	if s.groupManager == nil {
		return bizerr.New(bizerr.InternalServerError, "TiDB Group PrivateLink address evaluator is not configured")
	}
	state, err := s.observePrivateLinkIPAddressState(
		ctx,
		req.GetClusterId(),
		req.GetTidbGroupId(),
		target,
	)
	if err != nil {
		return err
	}
	if state.blocker != nil {
		if bizerr.Is(state.blocker, bizerr.PrivateLinkAddressPending) {
			return nil
		}
		return state.blocker
	}
	currentTarget := state.infrastructure.Requested
	if currentTarget != target && !state.status.GetReady() {
		return bizerr.New(
			bizerr.PreconditionFailed,
			fmt.Sprintf(
				"TiDB Group PrivateLink address transition to %s is still in progress; cannot start transition to %s",
				currentTarget,
				target,
			),
		)
	}
	var advanceErr error
	switch target {
	case v1alpha1.IPAddressTypeDualstack:
		advanceErr = s.advanceGroupToDualstack(ctx, req.GetClusterId(), req.GetTidbGroupId(), state)
	case v1alpha1.IPAddressTypeIPv4:
		advanceErr = s.advanceGroupToIPv4(ctx, req.GetClusterId(), req.GetTidbGroupId(), state)
	default:
		return bizerr.New(bizerr.ParameterInvalid, "unsupported ip_address_type "+string(target))
	}
	if bizerr.Is(advanceErr, bizerr.PrivateLinkAddressPending) {
		return nil
	}
	return advanceErr
}

func (s *PrivateLinkServiceService) advanceGroupToDualstack(
	ctx context.Context,
	clusterID string,
	groupID string,
	state *privateLinkIPAddressState,
) error {
	infrastructure := state.infrastructure
	if infrastructure.Requested != v1alpha1.IPAddressTypeDualstack {
		return s.setGroupAddressTypes(
			ctx,
			clusterID,
			groupID,
			v1alpha1.IPAddressTypeDualstack,
			"",
		)
	}
	ready, err := s.groupManager.EnsureIPv6Prerequisites(ctx, infrastructure)
	if err != nil {
		return bizerr.New(
			bizerr.InternalServerError,
			"failed to ensure TiDB Group PrivateLink IPv6 prerequisites: "+err.Error(),
		)
	}
	if !ready {
		return nil
	}
	if infrastructure.NLBTarget != v1alpha1.IPAddressTypeDualstack {
		return s.setGroupAddressTypes(
			ctx,
			clusterID,
			groupID,
			"",
			v1alpha1.IPAddressTypeDualstack,
		)
	}
	if !infrastructure.NLBReady {
		return nil
	}
	if state.plsTarget != v1alpha1.IPAddressTypeDualstack {
		return s.setPrivateLinkServiceAddressType(
			ctx, clusterID, groupID, state.normal, v1alpha1.IPAddressTypeDualstack,
		)
	}
	return nil
}

func (s *PrivateLinkServiceService) advanceGroupToIPv4(
	ctx context.Context,
	clusterID string,
	groupID string,
	state *privateLinkIPAddressState,
) error {
	// An empty target leaves the address type unmanaged by the EndpointService
	// controller. Persist an explicit IPv4 target only when AWS still needs to converge.
	plsNeedsExplicitIPv4Target := state.normal.pls.Spec.TargetIPAddressType == "" &&
		state.available && state.observed != v1alpha1.IPAddressTypeIPv4
	plsNeedsUpdate := state.plsTarget != v1alpha1.IPAddressTypeIPv4 || plsNeedsExplicitIPv4Target
	if state.infrastructure.Requested != v1alpha1.IPAddressTypeIPv4 || plsNeedsUpdate {
		if err := s.validateObservedGroupCanDisableIPv6(
			ctx,
			state.normal,
			state.available,
		); err != nil {
			return err
		}
	}
	if state.infrastructure.Requested != v1alpha1.IPAddressTypeIPv4 {
		return s.setGroupAddressTypes(
			ctx,
			clusterID,
			groupID,
			v1alpha1.IPAddressTypeIPv4,
			"",
		)
	}

	if plsNeedsUpdate {
		return s.setPrivateLinkServiceAddressType(
			ctx,
			clusterID,
			groupID,
			state.normal,
			v1alpha1.IPAddressTypeIPv4,
		)
	}
	if !state.available || state.observed != v1alpha1.IPAddressTypeIPv4 {
		return nil
	}
	if state.infrastructure.NLBTarget != v1alpha1.IPAddressTypeIPv4 {
		return s.setGroupAddressTypes(
			ctx,
			clusterID,
			groupID,
			"",
			v1alpha1.IPAddressTypeIPv4,
		)
	}
	return nil
}

func (s *PrivateLinkServiceService) setGroupAddressTypes(
	ctx context.Context,
	clusterID string,
	groupID string,
	requested v1alpha1.IPAddressType,
	nlb v1alpha1.IPAddressType,
) error {
	if s.groupClusterClient == nil {
		return bizerr.New(bizerr.InternalServerError, "Cluster Server client is not configured")
	}
	requestedPB, err := privateLinkServiceIPAddressTypeToClusterPB(requested)
	if err != nil {
		return err
	}
	nlbPB, err := privateLinkServiceIPAddressTypeToClusterPB(nlb)
	if err != nil {
		return err
	}
	if err := s.groupClusterClient.SetPrivateLinkIPAddressType(ctx, &clusterctlpb.SetPrivateLinkIPAddressTypeRequest{
		ClusterId:              clusterID,
		TidbGroupId:            groupID,
		RequestedIpAddressType: requestedPB,
		NlbIpAddressType:       nlbPB,
	}); err != nil {
		return bizerr.Wrap(
			bizerr.InternalServerError,
			"failed to persist TiDB Group PrivateLink transition targets",
			err,
		)
	}
	return nil
}

func privateLinkServiceIPAddressTypeToClusterPB(
	addressType v1alpha1.IPAddressType,
) (clusterctlpb.PrivateLinkIPAddressType, error) {
	switch addressType {
	case "":
		return clusterctlpb.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_UNSPECIFIED, nil
	case v1alpha1.IPAddressTypeIPv4:
		return clusterctlpb.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_IPV4, nil
	case v1alpha1.IPAddressTypeDualstack:
		return clusterctlpb.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_DUALSTACK, nil
	default:
		return 0, bizerr.New(bizerr.ParameterInvalid, "unsupported ip_address_type "+string(addressType))
	}
}

func (s *PrivateLinkServiceService) setPrivateLinkServiceAddressType(
	ctx context.Context,
	clusterID string,
	groupID string,
	original *normalPrivateLinkService,
	target v1alpha1.IPAddressType,
) error {
	key := client.ObjectKeyFromObject(original.pls)
	expectedUID := original.pls.UID
	err := retry.RetryOnConflict(retry.DefaultRetry, func() error {
		normal, err := s.normalGroupPrivateLinkService(
			ctx,
			clusterID,
			groupID,
			original.vpc,
			original.privateNLBARN,
		)
		if err != nil {
			return err
		}
		current := normal.pls
		if client.ObjectKeyFromObject(current) != key ||
			(expectedUID != "" && current.UID != expectedUID) {
			return bizerr.New(
				bizerr.PreconditionFailed,
				"the normal PrivateLinkService for the TiDB Group is no longer ready",
			)
		}
		if current.Spec.TargetIPAddressType == target {
			return nil
		}

		current.Spec.TargetIPAddressType = target
		return s.privatelinkservicerw.Update(ctx, current)
	})
	if err == nil {
		return nil
	}
	if _, ok := err.(bizerr.Error); ok {
		return err
	}
	return bizerr.Wrap(
		bizerr.K8SClientError,
		"failed to persist the normal PrivateLinkService address type",
		err,
	)
}

func effectivePrivateLinkServiceAddressType(addressType v1alpha1.IPAddressType) v1alpha1.IPAddressType {
	if addressType == "" {
		return v1alpha1.IPAddressTypeIPv4
	}
	return addressType
}

func privateLinkServiceIPAddressTypeFromPB(
	addressType regionalapiv1.PrivateLinkIPAddressType,
) (v1alpha1.IPAddressType, error) {
	switch addressType {
	case regionalapiv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_IPV4:
		return v1alpha1.IPAddressTypeIPv4, nil
	case regionalapiv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_DUALSTACK:
		return v1alpha1.IPAddressTypeDualstack, nil
	default:
		return "", bizerr.New(bizerr.ParameterInvalid, "unsupported ip_address_type "+addressType.String())
	}
}

func privateLinkServiceIPAddressTypeToPB(addressType v1alpha1.IPAddressType) regionalapiv1.PrivateLinkIPAddressType {
	switch addressType {
	case v1alpha1.IPAddressTypeDualstack:
		return regionalapiv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_DUALSTACK
	case v1alpha1.IPAddressTypeIPv4:
		return regionalapiv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_IPV4
	default:
		return regionalapiv1.PrivateLinkIPAddressType_PRIVATE_LINK_IP_ADDRESS_TYPE_UNSPECIFIED
	}
}

func (s *PrivateLinkServiceService) validatePrivateLinkGroupRequest(clusterID, groupID string) error {
	if s.config.Provider != consts.ProviderTypeEKS {
		return bizerr.New(bizerr.PreconditionFailed, "TiDB Group PrivateLink address type is only supported for EKS")
	}
	if clusterID == "" || groupID == "" {
		return bizerr.New(bizerr.ParameterInvalid, "cluster_id and tidb_group_id are required")
	}
	return nil
}
