package aks

import (
	"context"
	"errors"
	"fmt"
	"strings"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	azurenetworkv1beta1 "github.com/upbound/provider-azure/apis/network/v1beta1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/cloud/azure"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/privatelinkendpoint/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

type endpoint struct {
	*task.BaseTask
	azureCli azure.Client
}

func NewEndpoint(ctx *task.Context, azureCli azure.Client) deploy.Task {
	return &endpoint{
		BaseTask: &task.BaseTask{Context: ctx},
		azureCli: azureCli,
	}
}

func (t *endpoint) Name() string {
	return "Endpoint"
}

func (t *endpoint) Create(ctx context.Context) deploy.Result {
	resourceGroup := t.VPC.Status.ResourceGroup.ID
	if resourceGroup == "" {
		return deploy.Panic(fmt.Errorf("resource group is empty"))
	}
	serviceAlias := t.PrivateLinkEndpoint.Spec.ServiceName
	if serviceAlias == "" {
		return deploy.Panic(fmt.Errorf("endpoint service alias is empty"))
	}
	subnetID, err := t.getEndpointSubnetID()
	if err != nil {
		return deploy.Panic(err)
	}

	tags := t.EffectiveCloudTags(consts.ProviderTypeAKS)
	labels := util.MergeMaps(t.VPC.Labels, map[string]string{v1alpha1.PrivateLinkEndpointNameKey: t.PrivateLinkEndpoint.Name})

	pe := &azurenetworkv1beta1.PrivateEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.EndpointName(),
			Labels: labels,
		},
		Spec: azurenetworkv1beta1.PrivateEndpointSpec{
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.ProviderConfigName(),
				},
				ManagementPolicies: controllers.GetResourceManagementPolicies(),
			},
			ForProvider: azurenetworkv1beta1.PrivateEndpointParameters{
				Location:          ptr.To(t.PrivateLinkEndpoint.Spec.Region),
				ResourceGroupName: ptr.To(resourceGroup),
				SubnetID:          ptr.To(subnetID),
				PrivateServiceConnection: []azurenetworkv1beta1.PrivateServiceConnectionParameters{
					{
						Name:                           ptr.To(t.EndpointName()),
						IsManualConnection:             ptr.To(true),
						PrivateConnectionResourceAlias: ptr.To(serviceAlias),
						SubresourceNames:               getSubresourceNames(serviceAlias),
						RequestMessage:                 ptr.To(getRequestMessage(serviceAlias)),
					},
				},
				Tags: util.ToMapPointer(tags),
			},
		},
	}
	if err := k8s.Apply(ctx, t.Local, pe); err != nil {
		return deploy.Panic(err)
	}
	return deploy.Done()
}

func (t *endpoint) PostCreate(ctx context.Context) deploy.Result {
	t.PrivateLinkEndpoint.Status.Phase = v1alpha1.PrivateLinkEndpointPhaseCreating

	pe := &azurenetworkv1beta1.PrivateEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.EndpointName(),
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(pe), pe); err != nil {
		return deploy.Error(err)
	}
	if !controllers.IsResourceReady(pe.Status.ResourceStatus) {
		return deploy.Error(fmt.Errorf("private endpoint is not ready"))
	}

	if err := t.syncEndpointInfo(ctx, pe); err != nil {
		return deploy.Error(err)
	}
	if t.PrivateLinkEndpoint.Status.Phase != v1alpha1.PrivateLinkEndpointPhaseEndpointAvailable {
		return deploy.Error(fmt.Errorf("private endpoint is not ready, phase: %s", t.PrivateLinkEndpoint.Status.Phase))
	}
	return deploy.Done()
}

func (t *endpoint) syncEndpointInfo(ctx context.Context, pe *azurenetworkv1beta1.PrivateEndpoint) error {
	if len(pe.Status.AtProvider.PrivateServiceConnection) == 0 {
		t.PrivateLinkEndpoint.Status.Message = "private service connection is empty"
		return errors.New(t.PrivateLinkEndpoint.Status.Message)
	}

	privateIP := ptr.Deref(pe.Status.AtProvider.PrivateServiceConnection[0].PrivateIPAddress, "")
	if privateIP == "" {
		t.PrivateLinkEndpoint.Status.Message = "private ip is empty"
		return errors.New(t.PrivateLinkEndpoint.Status.Message)
	}

	t.PrivateLinkEndpoint.Status.ID = ptr.Deref(pe.Status.AtProvider.ID, "")
	t.PrivateLinkEndpoint.Status.EndpointAddress = privateIP

	// Query Azure SDK for the actual PSC connection state, since the Upbound
	// provider observation does not expose ManualPrivateLinkServiceConnections status.
	if t.azureCli != nil {
		phase, err := t.checkConnectionState(ctx)
		if err != nil {
			return err
		}
		t.PrivateLinkEndpoint.Status.Phase = phase
	} else {
		return errors.New("azure client is not initialized")
	}
	return nil
}

func (t *endpoint) checkConnectionState(ctx context.Context) (v1alpha1.PrivateLinkEndpointPhase, error) {
	subscriptionID := t.GetSubscriptionID()
	resourceGroup := t.VPC.Status.ResourceGroup.ID
	peName := t.EndpointName()

	peClient, err := t.azureCli.PrivateEndpoints(subscriptionID)
	if err != nil {
		return "", fmt.Errorf("failed to create private endpoints client: %w", err)
	}
	resp, err := peClient.Get(ctx, resourceGroup, peName)
	if err != nil {
		return "", fmt.Errorf("failed to get private endpoint %s: %w", peName, err)
	}

	if resp.Properties != nil &&
		len(resp.Properties.ManualPrivateLinkServiceConnections) > 0 &&
		resp.Properties.ManualPrivateLinkServiceConnections[0].Properties != nil &&
		resp.Properties.ManualPrivateLinkServiceConnections[0].Properties.PrivateLinkServiceConnectionState != nil &&
		resp.Properties.ManualPrivateLinkServiceConnections[0].Properties.PrivateLinkServiceConnectionState.Status != nil {
		return mapAzureConnectionStatus(
			*resp.Properties.ManualPrivateLinkServiceConnections[0].Properties.PrivateLinkServiceConnectionState.Status,
		), nil
	}
	return v1alpha1.PrivateLinkEndpointPhasePendingAcceptance, nil
}

func mapAzureConnectionStatus(status string) v1alpha1.PrivateLinkEndpointPhase {
	switch strings.ToLower(status) {
	case "approved":
		return v1alpha1.PrivateLinkEndpointPhaseEndpointAvailable
	case "rejected":
		return v1alpha1.PrivateLinkEndpointPhaseRejected
	case "pending":
		return v1alpha1.PrivateLinkEndpointPhasePendingAcceptance
	case "removed":
		return v1alpha1.PrivateLinkEndpointPhaseDeleting
	default:
		return v1alpha1.PrivateLinkEndpointPhaseFailed
	}
}

func (t *endpoint) Delete(ctx context.Context) deploy.Result {
	pe := &azurenetworkv1beta1.PrivateEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.EndpointName(),
		},
	}
	return deploy.PanicOrDone(controllers.DeleteUpboundResource(ctx, t.Local, pe))
}

func (t *endpoint) getEndpointSubnetID() (string, error) {
	if len(t.VPC.Status.NodeSubnets) > 0 && t.VPC.Status.NodeSubnets[0].SubnetID != "" {
		return t.VPC.Status.NodeSubnets[0].SubnetID, nil
	}
	return "", fmt.Errorf("endpoint subnet id is empty")
}

// getSubresourceNames returns the Azure sub-resource group names based on the service type.
// For Private Link Service alias connections, no sub-resource is needed (returns nil).
// For Azure-native resources like MySQL or Storage Accounts, the appropriate sub-resource
// must be specified for the private endpoint connection to succeed.
func getSubresourceNames(serviceName string) []*string {
	serviceLower := strings.ToLower(serviceName)
	if strings.Contains(serviceLower, "microsoft.dbformysql") ||
		strings.Contains(serviceLower, "/mysqlservers/") {
		return []*string{ptr.To("mysqlServer")}
	}
	if strings.Contains(serviceLower, "microsoft.storage/storageaccounts") {
		return []*string{ptr.To("blob")}
	}
	return nil
}

// getRequestMessage returns a context-specific approval request message based on the Azure
// service type being connected. This helps service owners understand the purpose of the
// private endpoint connection when reviewing approval requests.
func getRequestMessage(serviceName string) string {
	serviceLower := strings.ToLower(serviceName)
	if strings.Contains(serviceLower, "microsoft.dbformysql") ||
		strings.Contains(serviceLower, "/mysqlservers/") {
		return "approve the connection to use dm on tidbcloud"
	}
	if strings.Contains(serviceLower, "microsoft.storage/storageaccounts") {
		return "approve the connection to use import on tidbcloud"
	}
	return "approve the connection to use changefeed on tidbcloud"
}
