package aks

import (
	"context"
	"fmt"

	azureerrors "github.com/Azure/azure-sdk-for-go-extensions/pkg/errors"
	"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork/v6"
	"github.com/crossplane/crossplane-runtime/pkg/meta"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/azure"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkservice/task"
	"github.com/upbound/provider-azure/apis/network/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/errors"
	"k8s.io/utils/ptr"
)

type PrivateLinkEndpoint struct {
	*task.BaseTask
}

func NewPrivateLinkEndpoint(c *task.Context) *PrivateLinkEndpoint {
	return &PrivateLinkEndpoint{BaseTask: &task.BaseTask{Context: c}}
}

func (t *PrivateLinkEndpoint) Name() string {
	return "PrivateLinkEndpoint"
}

func (t *PrivateLinkEndpoint) Create(ctx context.Context) deploy.Result {
	resourceGroupName := controllers.GetResourceGroupName(t.VPC)
	serviceName := t.PrivateLinkService.Status.AzurePrivateLinkService.Name
	conns, err := t.PLSCli.ListPrivateLinkEndpointConnections(ctx, resourceGroupName, serviceName)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	connsMap := azure.GenerateConnsMap(conns)
	allowed := t.PrivateLinkService.Spec.Connections
	connStatus, unrejected := t.calculateConnsStatus(allowed, connsMap)
	unaccepted := controllers.GetUnacceptedVPCEndpoints(connStatus)
	errList := []error{}
	if len(unaccepted) > 0 {
		errs := t.acceptConnections(ctx, resourceGroupName, serviceName, unaccepted, connsMap)
		errList = append(errList, errs...)
	}

	if len(unrejected) > 0 {
		errs := t.rejectConnections(ctx, resourceGroupName, serviceName, unrejected...)
		errList = append(errList, errs...)
	}
	if len(errList) != 0 {
		return deploy.Panic(errors.NewAggregate(errList)).WithRequeuAfter(defaultRequeuInterval)
	}

	return deploy.Done()
}

func (t *PrivateLinkEndpoint) acceptConnections(ctx context.Context, resourceGroupName, serviceName string, unaccepted []string, allConns map[string]*armnetwork.PrivateEndpointConnection) []error {
	errList := make([]error, 0)
	for _, ep := range unaccepted {
		conn, ok := allConns[ep]
		if !ok {
			errList = append(errList, fmt.Errorf("the endpoint %s does not exist", ep))
		}
		err := t.PLSCli.AcceptPrivateLinkEndpointConnection(ctx, resourceGroupName, serviceName, conn)
		if err != nil {
			errList = append(errList, err)
		}
	}
	return errList
}

func (t *PrivateLinkEndpoint) rejectConnections(ctx context.Context, resourceGroupName, serviceName string, unrejected ...*armnetwork.PrivateEndpointConnection) []error {
	errList := make([]error, 0)
	for id := range unrejected {
		err := t.PLSCli.RejectPrivateLinkEndpointConnection(ctx, resourceGroupName, serviceName, unrejected[id])
		if err != nil {
			errList = append(errList, err)
		}
	}
	return errList
}

func (t *PrivateLinkEndpoint) calculateConnsStatus(allowed []v1alpha1.Connection, conns map[string]*armnetwork.PrivateEndpointConnection) ([]v1alpha1.ConnectionStatus, []*armnetwork.PrivateEndpointConnection) {
	allowedMap := make(map[string]struct{})
	connsStatus := make([]v1alpha1.ConnectionStatus, 0)
	unrejected := make([]*armnetwork.PrivateEndpointConnection, 0)

	for _, item := range allowed {
		conn, ok := conns[item.Endpoint]
		if !ok {
			status := v1alpha1.ConnectionStatus{
				Endpoint: item.Endpoint,
				Phase:    v1alpha1.ConnectionPhaseFailed,
				Message:  fmt.Sprintf("The endpoint %s does not exist", item.Endpoint),
			}
			connsStatus = append(connsStatus, status)
			continue
		}
		allowedMap[item.Endpoint] = struct{}{}
		connsStatus = append(connsStatus, coverConnStatus(conn))
	}

	for key, conn := range conns {
		// if the conn is not in the allowed list, and the state is approved, it should be rejected.
		if _, ok := allowedMap[ptr.Deref(conn.Properties.PrivateEndpoint.ID, "")]; !ok {
			state := ptr.Deref(conn.Properties.PrivateLinkServiceConnectionState.Status, "")
			if state == azure.ConnectionStateApproved {
				unrejected = append(unrejected, conns[key])
			}
		}
	}
	return connsStatus, unrejected
}

func coverConnStatus(conn *armnetwork.PrivateEndpointConnection) v1alpha1.ConnectionStatus {
	endpointID := ptr.Deref(conn.Properties.PrivateEndpoint.ID, "")
	status := v1alpha1.ConnectionStatus{
		Endpoint: endpointID,
	}
	state := ptr.Deref(conn.Properties.PrivateLinkServiceConnectionState.Status, "")
	switch state {
	case azure.ConnectionStatePending:
		status.Phase = v1alpha1.ConnectionPhasePending
	case azure.ConnectionStateApproved:
		status.Phase = v1alpha1.ConnectionPhaseActive
	case azure.ConnectionStateRejected:
		// if the connection is rejected in azure, it can't be approved again.
		// so we should return failed status here and let user to create a new endpoint.
		status.Phase = v1alpha1.ConnectionPhaseFailed
		status.Message = fmt.Sprintf("The endpoint %s is rejected, need to create a new endpoint", endpointID)
	case azure.ConnectionStateDisconnected:
		status.Phase = v1alpha1.ConnectionPhaseFailed
		status.Message = fmt.Sprintf("The endpoint %s is removed, need to create a new endpoint", endpointID)
	case azure.ConnectionStateEmpty:
		status.Phase = v1alpha1.ConnectionPhasePending
	default:
		status.Phase = v1alpha1.ConnectionPhaseFailed
		status.Message = fmt.Sprintf("The endpoint %s is in unknown state %s", endpointID, state)
	}
	return status
}

func (t *PrivateLinkEndpoint) PostCreate(ctx context.Context) deploy.Result {
	resourceGroupName := controllers.GetResourceGroupName(t.VPC)
	serviceName := t.PrivateLinkService.Status.AzurePrivateLinkService.Name
	conns, err := t.PLSCli.ListPrivateLinkEndpointConnections(ctx, resourceGroupName, serviceName)
	if err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	allowed := t.PrivateLinkService.Spec.Connections
	connStatus, unrejected := t.calculateConnsStatus(allowed, azure.GenerateConnsMap(conns))
	unaccepted := controllers.GetUnacceptedVPCEndpoints(connStatus)
	if len(unaccepted)+len(unrejected) > 0 {
		return deploy.Panic(fmt.Errorf("some endpoints are not accepted or rejected")).WithRequeuAfter(defaultRequeuInterval)
	}
	pendingAndNotAllowed := getPendingAndNotAllowedConns(allowed, azure.GenerateConnsMap(conns))
	if len(pendingAndNotAllowed) > 0 {
		connStatus = append(connStatus, pendingAndNotAllowed...)
	}
	if err := t.setConnectionStatus(ctx, connStatus); err != nil {
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	return deploy.Done()
}

func getPendingAndNotAllowedConns(allowed []v1alpha1.Connection, conns map[string]*armnetwork.PrivateEndpointConnection) []v1alpha1.ConnectionStatus {
	allowedMap := make(map[string]v1alpha1.Connection, len(allowed))
	output := make([]v1alpha1.ConnectionStatus, 0)
	for _, item := range allowed {
		allowedMap[item.Endpoint] = item
	}
	for _, conn := range conns {
		state := ptr.Deref(conn.Properties.PrivateLinkServiceConnectionState.Status, "")
		if state != azure.ConnectionStatePending {
			continue
		}
		endpintID := ptr.Deref(conn.Properties.PrivateEndpoint.ID, "")
		if endpintID == "" {
			continue
		}
		if _, ok := allowedMap[endpintID]; !ok {
			output = append(output, coverConnStatus(conn))
		}
	}
	return output
}

func (t *PrivateLinkEndpoint) setConnectionStatus(ctx context.Context, conns []v1alpha1.ConnectionStatus) error {
	t.PrivateLinkService.Status.Connections = conns
	return t.PrivateLinkServiceRW.UpdateStatus(ctx, t.PrivateLinkService)
}

func (t *PrivateLinkEndpoint) Delete(ctx context.Context) deploy.Result {
	ves := &v1beta1.PrivateLinkService{}
	if err := t.Local.Get(ctx, types.NamespacedName{Name: getPrivateLinkServiceResourceName(t.PrivateLinkService)}, ves); err != nil {
		if apierrors.IsNotFound(err) {
			return deploy.Done()
		}
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	resourceGroupName := controllers.GetResourceGroupName(t.VPC)
	serviceName := meta.GetExternalName(ves)
	if serviceName == "" {
		t.Logger.Info("the privatelink service name is empty, represent the service is not created successfully, skip deleting connections")
		return deploy.Done()
	}
	conns, err := t.PLSCli.ListPrivateLinkEndpointConnections(ctx, resourceGroupName, serviceName)
	if err != nil {
		if azureerrors.IsNotFoundErr(err) {
			t.Logger.Info("the privatelink connections are not found, skip deleting connections")
			return deploy.Done()
		}
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	errs := t.removeAllConns(ctx, resourceGroupName, serviceName, conns...)
	if len(errs) != 0 {
		return deploy.Panic(errors.NewAggregate(errs)).WithRequeuAfter(defaultRequeuInterval)
	}
	return deploy.Done()
}

func (t *PrivateLinkEndpoint) removeAllConns(ctx context.Context, resourceGroupName, serviceName string, conns ...*armnetwork.PrivateEndpointConnection) []error {
	errList := make([]error, 0)
	for idx := range conns {
		err := t.PLSCli.RemovePrivateLinkEndpointConnection(ctx, resourceGroupName, serviceName, conns[idx])
		if err != nil {
			errList = append(errList, err)
		}
	}
	return errList
}

func (t *PrivateLinkEndpoint) PostDelete(ctx context.Context) deploy.Result {
	ves := &v1beta1.PrivateLinkService{}
	if err := t.Local.Get(ctx, types.NamespacedName{Name: getPrivateLinkServiceResourceName(t.PrivateLinkService)}, ves); err != nil {
		if apierrors.IsNotFound(err) {
			return deploy.Done()
		}
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	resourceGroupName := controllers.GetResourceGroupName(t.VPC)
	serviceName := meta.GetExternalName(ves)
	if serviceName == "" {
		t.Logger.Info("the privatelink service name is empty, represent the service is not created successfully, skip deleting connections")
		return deploy.Done()
	}
	conns, err := t.PLSCli.ListPrivateLinkEndpointConnections(ctx, resourceGroupName, serviceName)
	if err != nil {
		if azureerrors.IsNotFoundErr(err) {
			t.Logger.Info("the privatelink connections are not found, delete successfully")
			return deploy.Done()
		}
		return deploy.Panic(err).WithRequeuAfter(defaultRequeuInterval)
	}
	if len(conns) != 0 {
		return deploy.Panic(fmt.Errorf("the privatelink service %s still has %d connections", serviceName, len(conns))).WithRequeuAfter(defaultRequeuInterval)
	}
	return deploy.Done()
}
