package commonazure

import (
	"context"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/pingcap/errors"
	azurenetworkv1beta2 "github.com/upbound/provider-azure/apis/network/v1beta2"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"

	agentv2 "github.com/tidbcloud/observability/agent-control-server/api/v2"
	"github.com/tidbcloud/observability/agent-control-server/pkg/agentbuild"
	azurecloud "github.com/tidbcloud/observability/agent-control-server/pkg/cloud/azure"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
	"github.com/tidbcloud/observability/agent-control-server/pkg/deploy"
	"github.com/tidbcloud/observability/agent-control-server/pkg/reconciler"
	"github.com/tidbcloud/observability/agent-control-server/pkg/util"
	"github.com/tidbcloud/observability/agent-control-server/pkg/util/namer"
)

// PrivateLinkConfig holds the agent-specific fields needed by PrivateLinkTask.
type PrivateLinkConfig struct {
	// AKS returns the AKS configuration from the agent spec.
	// Must not return nil.
	AKS func() *agentv2.AKSConfig

	// KubernetesClusterName returns the cluster name from the agent spec.
	KubernetesClusterName func() string

	// ObjectMeta returns the agent's ObjectMeta, used to derive the endpoint name.
	ObjectMeta func() metav1.ObjectMeta

	// ClusterMetaFunc optionally provides a ClusterMeta override.
	// If nil, the default empty ClusterMeta from BaseTask is used.
	ClusterMetaFunc func() deploy.ClusterMeta
}

// PrivateLinkTask creates Azure Private Endpoint to send o11y data to o11y cluster.
//
// This shared implementation eliminates code duplication between o11yk8sagent,
// o11ytidbagent, and kubernetesagent.
type PrivateLinkTask[T client.Object] struct {
	*reconciler.BaseTask[T]
	cfg PrivateLinkConfig
}

func NewPrivateLinkTask[T client.Object](ctx *reconciler.Context[T], cfg PrivateLinkConfig) *PrivateLinkTask[T] {
	return &PrivateLinkTask[T]{
		BaseTask: &reconciler.BaseTask[T]{Context: ctx},
		cfg:      cfg,
	}
}

func (t *PrivateLinkTask[T]) Name() string {
	return "PrivateLink"
}

func (t *PrivateLinkTask[T]) ClusterMeta() deploy.ClusterMeta {
	if t.cfg.ClusterMetaFunc != nil {
		return t.cfg.ClusterMetaFunc()
	}
	return deploy.ClusterMeta{}
}

func (t *PrivateLinkTask[T]) buildTags(endpointName string) map[string]*string {
	return map[string]*string{
		consts.CrossplaneKeyVendor:    ptr.To(consts.CrossplaneValueVendor),
		consts.CrossplaneKeyName:      ptr.To(endpointName),
		consts.CrossplaneKeyComponent: ptr.To(consts.CrossplaneValueComponent),
		consts.CrossplaneKeyTenant:    ptr.To(util.GetLabel(t.Agent, agentv2.TagKeyTenant)),
	}
}

func (t *PrivateLinkTask[T]) Create(ctx context.Context) deploy.Result {
	needUpdate := util.ShouldUpdateO11yVersion(t.Agent.GetAnnotations(), t.Config.O11yConfig.Version)
	endpoint := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName)
	if endpoint != "" && !needUpdate {
		return deploy.Done()
	}

	endpointServiceName := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yEndpointServiceName)
	if endpointServiceName == "" {
		return deploy.Error(errors.New("endpoint service name not found, wait for o11y cluster ready"))
	}

	endpointName, err := azurecloud.FindReusablePrivateEndpointName(ctx, t.Local, endpointServiceName, t.cfg.AKS().SubnetID)
	if err != nil {
		return deploy.Error(errors.Annotate(err, "failed to find reusable private endpoint"))
	}
	if endpointName != "" {
		t.L.Info("found an existing private endpoint, reusing it", "name", endpointName)
		util.SetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName, endpointName)
		return deploy.Done()
	}

	endpointName = t.endpointName()
	if err := t.createPrivateEndpoint(ctx, endpointServiceName, endpointName); err != nil {
		return deploy.Error(errors.Annotate(err, "failed to create private endpoint"))
	}

	t.L.Info("create private endpoint success", "name", endpointName)
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName, endpointName)
	return deploy.Done()
}

func (t *PrivateLinkTask[T]) createPrivateEndpoint(ctx context.Context, endpointServiceName, endpointName string) error {
	region := t.Config.O11yConfig.Region
	if region == "" {
		return errors.New("Azure region is not configured")
	}

	aks := t.cfg.AKS()
	endpoint := &azurenetworkv1beta2.PrivateEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name: endpointName,
		},
	}

	_, err := controllerutil.CreateOrUpdate(ctx, t.Local, endpoint, func() error {
		endpoint.Spec.ProviderConfigReference = aks.ProviderConfigReference
		endpoint.Spec.ForProvider.Location = ptr.To(region)
		endpoint.Spec.ForProvider.ResourceGroupName = ptr.To(aks.ResourceGroup)
		endpoint.Spec.ForProvider.SubnetID = ptr.To(aks.SubnetID)
		endpoint.Spec.ForProvider.PrivateServiceConnection = &azurenetworkv1beta2.PrivateServiceConnectionParameters{
			Name:               ptr.To(endpointName),
			IsManualConnection: ptr.To(true),
			RequestMessage:     ptr.To("PingCAP o11y private endpoint connection"),
		}
		// Place the alias and resource ID in InitProvider so that both fields
		// are added to terraform ignore_changes after creation. Azure resolves
		// the alias into a resource ID, which would otherwise cause Crossplane
		// to detect drift and attempt a force-replace.
		endpoint.Spec.InitProvider.PrivateServiceConnection = &azurenetworkv1beta2.PrivateServiceConnectionInitParameters{
			PrivateConnectionResourceAlias: ptr.To(endpointServiceName),
			PrivateConnectionResourceID:    ptr.To(""),
		}
		endpoint.Spec.ForProvider.Tags = t.buildTags(endpointName)

		if endpoint.Labels == nil {
			endpoint.Labels = make(map[string]string)
		}
		endpoint.Labels[consts.LabelKeyRelatedCluster] = t.cfg.KubernetesClusterName()
		endpoint.Labels[agentv2.TagKeyTenant] = util.GetLabel(t.Agent, agentv2.TagKeyTenant)
		return nil
	})
	if err != nil {
		return errors.Annotate(err, "failed to create/update private endpoint")
	}
	return nil
}

func (t *PrivateLinkTask[T]) PostCreate(ctx context.Context) deploy.Result {
	endpointName := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName)
	if endpointName == "" {
		return deploy.Done()
	}

	var ep azurenetworkv1beta2.PrivateEndpoint
	if err := t.Local.Get(ctx, client.ObjectKey{Name: endpointName}, &ep); err != nil {
		return deploy.Error(errors.Wrap(err, "failed to get private endpoint"))
	}

	if condition := ep.Status.GetCondition(xpv1.TypeReady); condition.Status != corev1.ConditionTrue {
		return deploy.Error(errors.New("private endpoint is not ready"))
	}

	ip := azurecloud.ExtractPrivateIP(&ep)
	if ip == "" {
		return deploy.Error(errors.New("private endpoint IP not found"))
	}

	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceDNSName, ip)
	return deploy.Done()
}

func (t *PrivateLinkTask[T]) Delete(ctx context.Context) deploy.Result {
	endpointName := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName)
	if endpointName == "" {
		endpointName = t.endpointName()
	}

	endpoint := &azurenetworkv1beta2.PrivateEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name: endpointName,
		},
	}
	if err := agentbuild.DeleteObject(ctx, t.Local, endpoint); err != nil {
		return deploy.Error(errors.Annotate(err, "failed to delete private endpoint"))
	}
	return deploy.Done()
}

func (t *PrivateLinkTask[T]) endpointName() string {
	return namer.UniqueIdentifier(t.cfg.ObjectMeta(), namer.WithPrefix(consts.O11yEndpointNamePrefixDataplaneNextGen))
}
