package privatelinkgroup

import (
	"context"
	"errors"
	"fmt"

	elbv2types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
	perrors "github.com/pingcap/errors"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/util"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"sigs.k8s.io/controller-runtime/pkg/client"
)

// Manager owns shared resource resolution, idempotent IPv6 prerequisite setup,
// and live AWS readback.
type Manager struct {
	reader    client.Reader
	awsClient aws.Client

	vpcClient VPCClient
	lbClient  LoadBalancerClient
}

type Evaluation struct {
	Requested     v1alpha1.IPAddressType
	NLBTarget     v1alpha1.IPAddressType
	NLBReady      bool
	Message       string
	PrivateNLBARN string

	resources *resolvedResources
}

var (
	// ErrPrivateNLBDisabled is returned when the TiDB Group has no private load
	// balancer to back its PrivateLink service.
	ErrPrivateNLBDisabled = errors.New("private NLB is not enabled")
	// ErrTiDBGroupNotReady is returned while a management-service TiDB Group has
	// not yet been propagated to the Cluster CR.
	ErrTiDBGroupNotReady  = errors.New("TiDB group is not ready in Cluster")
	errPrivateNLBNotReady = errors.New("private NLB is not ready")
)

func NewManager(reader client.Reader, awsClient aws.Client) *Manager {
	return &Manager{reader: reader, awsClient: awsClient}
}

func (m *Manager) Evaluate(ctx context.Context, clusterID, groupID string) (Evaluation, error) {
	group, resources, err := m.loadResources(ctx, clusterID, groupID)
	if err != nil {
		if errors.Is(err, errPrivateNLBNotReady) {
			return Evaluation{
				Requested:     group.requested,
				NLBTarget:     group.nlbTarget,
				Message:       "waiting for PrivateLink NLB to be created",
				PrivateNLBARN: group.privateNLBARN,
				resources:     &resources,
			}, nil
		}
		return Evaluation{}, err
	}
	vpcReady := true
	if group.requested == v1alpha1.IPAddressTypeDualstack {
		vpcReady, err = VPCIPv6Ready(
			ctx,
			resources.vpcClient,
			resources.vpcID,
			resources.nlb.SubnetIDs,
		)
		if err != nil {
			return Evaluation{}, err
		}
	}
	evaluation := evaluateObservation(group.requested, group.nlbTarget, resources.nlb, vpcReady)
	evaluation.PrivateNLBARN = group.privateNLBARN
	evaluation.resources = &resources
	return evaluation, nil
}

// EnsureIPv6Prerequisites associates VPC and NLB-subnet IPv6 CIDRs. It does
// not mutate the TiDB Group, NLB address type, PLS, or Endpoint Service.
func (m *Manager) EnsureIPv6Prerequisites(
	ctx context.Context,
	evaluation Evaluation,
) (bool, error) {
	if evaluation.resources == nil || evaluation.PrivateNLBARN == "" {
		return false, nil
	}
	ready, err := EnsureVPCIPv6(
		ctx,
		evaluation.resources.vpcClient,
		evaluation.resources.vpcID,
		evaluation.resources.nlb.SubnetIDs,
	)
	if err != nil {
		return false, err
	}
	return ready, nil
}

// VPC returns the VPC resolved by Evaluate for reuse by the caller.
func (e Evaluation) VPC() *v1alpha1.VPC {
	if e.resources == nil || e.resources.vpc == nil {
		return nil
	}
	return e.resources.vpc.DeepCopy()
}

type groupState struct {
	requested         v1alpha1.IPAddressType
	nlbTarget         v1alpha1.IPAddressType
	privateNLBEnabled bool
	privateNLBARN     string
}

type resolvedResources struct {
	vpcID     string
	vpcClient VPCClient
	nlb       NLBObservation
	vpc       *v1alpha1.VPC
}

func (m *Manager) loadResources(ctx context.Context, clusterID, groupID string) (groupState, resolvedResources, error) {
	group, err := m.loadGroup(ctx, clusterID, groupID)
	if err != nil {
		return groupState{}, resolvedResources{}, err
	}
	if !group.privateNLBEnabled {
		return group, resolvedResources{}, ErrPrivateNLBDisabled
	}
	resources, err := m.resolveResources(ctx, clusterID, group.privateNLBARN)
	return group, resources, err
}

func (m *Manager) loadGroup(ctx context.Context, clusterID, groupID string) (groupState, error) {
	if m.reader == nil {
		return groupState{}, perrors.New("Kubernetes reader is not initialized")
	}
	clusters := &cloudv1.ClusterList{}
	if err := m.reader.List(ctx, clusters, client.MatchingLabels{cloudv1.ClusterTagKey: clusterID}); err != nil {
		return groupState{}, perrors.Wrapf(err, "failed to list Cluster %s", clusterID)
	}
	if len(clusters.Items) != 1 {
		return groupState{}, perrors.Errorf("expected one Cluster for %s, got %d", clusterID, len(clusters.Items))
	}
	cluster := &clusters.Items[0]
	if cluster.Spec.Kubernetes.Provider != consts.ProviderTypeEKS {
		return groupState{}, perrors.New("PrivateLink group address type is only supported for EKS")
	}
	endpoints, ok := cluster.GetSQLEndpoints(groupID)
	if !ok {
		return groupState{}, perrors.Wrapf(
			ErrTiDBGroupNotReady,
			"TiDB group %s was not found in Cluster %s",
			groupID,
			clusterID,
		)
	}
	state := groupState{
		requested:         v1alpha1.IPAddressTypeIPv4,
		nlbTarget:         v1alpha1.IPAddressTypeIPv4,
		privateNLBEnabled: endpoints.Private != nil && endpoints.Private.Enabled != nil && *endpoints.Private.Enabled,
		privateNLBARN:     privateNLBARN(cluster, groupID, endpoints),
	}
	if endpoints.PrivateLinkIPAddressType != "" {
		requested, err := privateLinkIPAddressTypeFromCluster(
			groupID,
			"requested",
			endpoints.PrivateLinkIPAddressType,
		)
		if err != nil {
			return groupState{}, err
		}
		state.requested = requested
	}
	if endpoints.PrivateLinkNLBIPAddressType != "" {
		nlbTarget, err := privateLinkIPAddressTypeFromCluster(
			groupID,
			"NLB target",
			endpoints.PrivateLinkNLBIPAddressType,
		)
		if err != nil {
			return groupState{}, err
		}
		state.nlbTarget = nlbTarget
	}
	return state, nil
}

func privateLinkIPAddressTypeFromCluster(
	groupID string,
	field string,
	addressType cloudv1.PrivateLinkIPAddressType,
) (v1alpha1.IPAddressType, error) {
	switch addressType {
	case cloudv1.PrivateLinkIPAddressTypeIPv4:
		return v1alpha1.IPAddressTypeIPv4, nil
	case cloudv1.PrivateLinkIPAddressTypeDualstack:
		return v1alpha1.IPAddressTypeDualstack, nil
	default:
		return "", perrors.Errorf(
			"TiDB group %s has unsupported PrivateLink %s IP address type %q",
			groupID,
			field,
			addressType,
		)
	}
}

func privateNLBARN(cluster *cloudv1.Cluster, groupID string, endpoints *cloudv1.SqlEndpoints) string {
	if endpoints.Private == nil || endpoints.Private.Enabled == nil || !*endpoints.Private.Enabled {
		return ""
	}
	status, ok := cluster.GetSQLEndpointStatus(groupID)
	if !ok || status.PrivateEndpoint == nil {
		return ""
	}
	return status.PrivateEndpoint.ResourceID
}

func (m *Manager) resolveResources(
	ctx context.Context,
	clusterID string,
	privateNLBARN string,
) (resolvedResources, error) {
	routes := &v1alpha1.APPRouteList{}
	if err := m.reader.List(ctx, routes, client.MatchingLabels{v1alpha1.ClusterIDKey: clusterID}); err != nil {
		return resolvedResources{}, perrors.Wrapf(err, "failed to list APPRoute for Cluster %s", clusterID)
	}
	if len(routes.Items) != 1 {
		return resolvedResources{}, perrors.Errorf("expected one APPRoute for Cluster %s, got %d", clusterID, len(routes.Items))
	}
	route := &routes.Items[0]
	if route.Spec.VPCRef == nil {
		return resolvedResources{}, perrors.Errorf("APPRoute %s/%s has no VPC reference", route.Namespace, route.Name)
	}
	vpc := &v1alpha1.VPC{}
	if err := m.reader.Get(ctx, client.ObjectKey{Name: route.Spec.VPCRef.Name, Namespace: route.Spec.VPCRef.Namespace}, vpc); err != nil {
		return resolvedResources{}, perrors.Wrapf(err, "failed to get VPC %s/%s", route.Spec.VPCRef.Namespace, route.Spec.VPCRef.Name)
	}
	if vpc.Spec.Provider != consts.ProviderTypeEKS || vpc.Status.VPC.VPCID == "" || vpc.Spec.AccountRef == nil {
		return resolvedResources{}, perrors.Errorf("EKS VPC %s/%s is not ready", vpc.Namespace, vpc.Name)
	}
	resources := resolvedResources{
		vpcID: vpc.Status.VPC.VPCID,
		vpc:   vpc.DeepCopy(),
	}
	if privateNLBARN == "" {
		return resources, errPrivateNLBNotReady
	}
	vpcClient, lbClient := m.vpcClient, m.lbClient
	if vpcClient == nil || lbClient == nil {
		if m.awsClient == nil {
			return resolvedResources{}, perrors.New("AWS client is not initialized")
		}
		role := &aws.AssumedRole{RoleARN: util.GetRoleARN(vpc.Spec.AccountRef.AccountID, vpc.Spec.AccountRef.Role)}
		vpcClient = m.awsClient.EC2(role, aws.WithRegion(vpc.Spec.Region))
		lbClient = m.awsClient.ELBV2(role, aws.WithRegion(vpc.Spec.Region))
	}
	nlb, err := DescribeNLB(ctx, lbClient, privateNLBARN)
	if err != nil {
		return resolvedResources{}, err
	}
	if len(nlb.SubnetIDs) == 0 {
		return resolvedResources{}, perrors.Errorf("PrivateLink NLB %s has no attached subnets", nlb.ARN)
	}
	if nlb.VPCID != vpc.Status.VPC.VPCID {
		return resolvedResources{}, perrors.Errorf(
			"PrivateLink NLB %s belongs to VPC %s, not %s",
			nlb.ARN,
			nlb.VPCID,
			vpc.Status.VPC.VPCID,
		)
	}
	resources.vpcClient = vpcClient
	resources.nlb = nlb
	return resources, nil
}

func evaluateObservation(
	requested v1alpha1.IPAddressType,
	nlbTarget v1alpha1.IPAddressType,
	nlb NLBObservation,
	vpcReady bool,
) Evaluation {
	evaluation := Evaluation{
		Requested: requested,
		NLBTarget: nlbTarget,
		Message:   fmt.Sprintf("waiting for PrivateLink NLB to become %s", nlbTarget),
	}
	if !vpcReady {
		evaluation.Message = "waiting for VPC and attached NLB subnet IPv6 CIDRs"
		return evaluation
	}
	if nlb.State != elbv2types.LoadBalancerStateEnumActive {
		state := string(nlb.State)
		if state == "" {
			state = "unknown"
		}
		evaluation.Message = fmt.Sprintf("waiting for PrivateLink NLB to become active (current state: %s)", state)
		return evaluation
	}
	if nlb.AddressType != nlbTarget {
		return evaluation
	}
	evaluation.NLBReady = true
	evaluation.Message = fmt.Sprintf("PrivateLink NLB is ready for %s", nlbTarget)
	return evaluation
}
