package alicloud

import (
	"context"
	"slices"

	privatelink "github.com/alibabacloud-go/privatelink-20200415/v5/client"
	vpc "github.com/alibabacloud-go/vpc-20160428/v6/client"
	aliplv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/privatelink/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/pingcap/errors"
	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/controllers/kubernetesagent/metadata"
	"github.com/tidbcloud/observability/agent-control-server/pkg/agentbuild"
	"github.com/tidbcloud/observability/agent-control-server/pkg/cloud/alicloud"
	"github.com/tidbcloud/observability/agent-control-server/pkg/cloud/alicloud/alicloudupbound"
	"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"
)

// PrivateLinkTask creates and manages a VPC Interface Endpoint that allows
// the agent to send observability data to the o11y control plane privately
// over Alibaba Cloud PrivateLink.
type PrivateLinkTask struct {
	*reconciler.BaseTask[*agentv2.KubernetesAgent]
}

func NewPrivateLinkTask(ctx *reconciler.Context[*agentv2.KubernetesAgent]) *PrivateLinkTask {
	return &PrivateLinkTask{
		BaseTask: &reconciler.BaseTask[*agentv2.KubernetesAgent]{Context: ctx},
	}
}

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

func (t *PrivateLinkTask) ClusterMeta() deploy.ClusterMeta {
	return metadata.ClusterMeta(t.Agent)
}

func (t *PrivateLinkTask) buildVPCEndpointTags(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)),
	}
}

// findZoneAndVSwitch discovers the endpoint service and finds a zone and vSwitch.
// This is a shared helper that queries the AliCloud API to:
// 1. List available zones for the endpoint service
// 2. Pick the first zone alphabetically for idempotency
// 3. Find a vSwitch in that zone within the configured VPC
//
// Returns the chosen zone ID and vSwitch ID.
func (t *PrivateLinkTask) findZoneAndVSwitch(endpointServiceName string, enableAssumeRole bool) (string, string, error) {
	region := t.Config.O11yConfig.Region
	if region == "" {
		return "", "", errors.New("AliCloud region is not configured")
	}

	var assumeRole *alicloud.AssumedRole
	if enableAssumeRole {
		assumeRole = &alicloud.AssumedRole{
			RoleARN: t.Agent.Spec.ACK.AssumeRoleArn,
		}
	}

	// Discover the endpoint service and available zones via PrivateLink API.
	plCli, err := t.AliCloud.PrivateLink(assumeRole, alicloud.WithRegion(region))
	if err != nil {
		return "", "", errors.Wrap(err, "failed to create privatelink client")
	}

	svcOut, err := plCli.ListVpcEndpointServicesByEndUser(&privatelink.ListVpcEndpointServicesByEndUserRequest{
		RegionId:    ptr.To(region),
		ServiceName: ptr.To(endpointServiceName),
	})
	if err != nil {
		return "", "", errors.Wrapf(err, "failed to list vpc endpoint services by the service name: %s", endpointServiceName)
	}
	if svcOut.Body == nil || len(svcOut.Body.Services) == 0 {
		return "", "", errors.New("no endpoint service returned")
	}

	// Collect and normalize available zones from the endpoint service.
	var zones []string
	for _, z := range svcOut.Body.Services[0].Zones {
		if z != nil && *z != "" {
			zones = append(zones, *z)
		}
	}
	slices.Sort(zones)
	if len(zones) == 0 {
		return "", "", errors.New("no zones found for the endpoint service")
	}

	// Query a vSwitch in the chosen zone.
	vpcCli, err := t.AliCloud.VPC(&alicloud.AssumedRole{
		RoleARN: t.Agent.Spec.ACK.AssumeRoleArn,
	}, alicloud.WithRegion(region))
	if err != nil {
		return "", "", errors.Wrap(err, "failed to create vpc client")
	}

	// Try each zone in sorted order until we find one with a valid vSwitch.
	for _, chosenZone := range zones {
		vswitchOut, err := vpcCli.DescribeVSwitches(&vpc.DescribeVSwitchesRequest{
			RegionId: ptr.To(region),
			VpcId:    ptr.To(t.Agent.Spec.ACK.VpcID),
			ZoneId:   ptr.To(chosenZone),
		})
		if err != nil {
			return "", "", errors.Wrap(err, "failed to describe vswitches")
		}
		if vswitchOut.Body == nil || vswitchOut.Body.VSwitches == nil || len(vswitchOut.Body.VSwitches.VSwitch) == 0 {
			t.L.Info("no vswitch found in zone, trying next zone", "vpc", t.Agent.Spec.ACK.VpcID, "zone", chosenZone)
			continue
		}

		var vswitchIDs []string
		for _, vsw := range vswitchOut.Body.VSwitches.VSwitch {
			if vsw != nil && vsw.VSwitchId != nil {
				vswitchIDs = append(vswitchIDs, *vsw.VSwitchId)
			}
		}
		if len(vswitchIDs) == 0 {
			t.L.Info("no valid vswitch IDs found in zone, trying next zone", "vpc", t.Agent.Spec.ACK.VpcID, "zone", chosenZone)
			continue
		}
		slices.Sort(vswitchIDs)

		return chosenZone, vswitchIDs[0], nil
	}

	return "", "", errors.Errorf("no vswitch found in vpc %s across all available zones %v", t.Agent.Spec.ACK.VpcID, zones)
}

func (t *PrivateLinkTask) appendRelatedClusterTag(cli alicloud.PrivateLinkClient, region, endpointID string) error {
	_, err := cli.TagResources(&privatelink.TagResourcesRequest{
		RegionId:     ptr.To(region),
		ResourceId:   []*string{ptr.To(endpointID)},
		ResourceType: ptr.To("vpcendpoint"),
		Tag: []*privatelink.TagResourcesRequestTag{{
			Key:   ptr.To(consts.LabelKeyRelatedCluster),
			Value: ptr.To(t.Agent.Spec.KubernetesClusterName),
		}},
	})
	return err
}

// getExistingEndpointDomain fetches the domain for an existing endpoint using native API.
func (t *PrivateLinkTask) getExistingEndpointDomain(cli *privatelink.Client, region, endpointID string) deploy.Result {
	describeResp, err := cli.GetVpcEndpointAttribute(&privatelink.GetVpcEndpointAttributeRequest{
		RegionId:   ptr.To(region),
		EndpointId: ptr.To(endpointID),
	})
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to get vpc endpoint attribute"))
	}

	if describeResp.Body == nil || describeResp.Body.EndpointDomain == nil {
		return deploy.Error(errors.New("vpc endpoint domain not found in response"))
	}

	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceDNSName, *describeResp.Body.EndpointDomain)
	return deploy.Done()
}

// Create ensures there is an Interface Endpoint for the target o11y endpoint
// service and records its name on the Agent annotations. The flow is:
//  1. Skip if an endpoint already exists and the o11y version has not changed.
//  2. Try to reuse an existing endpoint in the same VPC and service.
//  3. Otherwise, create a new Interface Endpoint and bind a zone.
func (t *PrivateLinkTask) 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"))
	}

	if t.Config.IsServerlessMode() {
		return t.createWithNativeAPI(endpointServiceName)
	}
	return t.createWithUpbound(ctx, endpointServiceName)
}

func (t *PrivateLinkTask) createWithNativeAPI(endpointServiceName string) deploy.Result {
	region := t.Config.O11yConfig.Region
	cli, err := t.AliCloud.PrivateLink(nil, alicloud.WithRegion(region))
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to create privatelink client"))
	}

	endpointID, endpointTags, err := alicloud.FindReusableVPCEndpointID(cli, region, alicloud.VpcEndpointTypeInterface, endpointServiceName, t.Agent.Spec.ACK.VpcID)
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to find reusable vpc endpoint"))
	}
	if endpointID != "" {
		t.L.Info("found an existing endpoint, reusing it", "endpointID", endpointID)
		// Ensure the vpc endpoint can be found and used in sharedpoolagents
		if _, ok := endpointTags[consts.LabelKeyRelatedCluster]; !ok {
			if err := t.appendRelatedClusterTag(cli, region, endpointID); err != nil {
				return deploy.Error(errors.Wrap(err, "failed to tag vpc endpoint"))
			}
			t.L.Info("tagged existing endpoint with cluster name", "endpointID", endpointID)
		}

		util.SetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName, endpointID)
		return t.getExistingEndpointDomain(cli, region, endpointID)
	}

	dnsName := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceOriginDNSName)
	if dnsName == "" {
		return deploy.Error(errors.New("service dns name not found"))
	}

	zoneID, vswitchID, err := t.findZoneAndVSwitch(endpointServiceName, false)
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to pick zone and vswitch"))
	}

	endpointName := util.InterfaceEndpointName(t.Agent.ObjectMeta, dnsName, consts.O11yEndpointNamePrefixDataplaneNextGen)
	tags := t.buildVPCEndpointTags(endpointName)
	tags[consts.LabelKeyRelatedCluster] = ptr.To(t.Agent.Spec.KubernetesClusterName)

	endpointID, endpointDomain, err := t.createVpcEndpointNativeAPI(
		cli,
		region,
		endpointServiceName,
		endpointName,
		tags,
		zoneID,
		vswitchID,
	)
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to create interface endpoint"))
	}

	t.L.Info("create interface endpoint success", "endpointID", endpointID, "endpointDomain", endpointDomain)
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName, endpointID)
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceDNSName, endpointDomain)
	return deploy.Done()
}

func (t *PrivateLinkTask) createVpcEndpointNativeAPI(cli *privatelink.Client, region, endpointServiceName, endpointName string, tags map[string]*string, zoneID, vswitchID string) (endpointID, endpointDomain string, err error) {
	tagList := make([]*privatelink.CreateVpcEndpointRequestTag, 0, len(tags))
	for k, v := range tags {
		tagList = append(tagList, &privatelink.CreateVpcEndpointRequestTag{
			Key:   ptr.To(k),
			Value: v,
		})
	}

	createReq := &privatelink.CreateVpcEndpointRequest{
		RegionId:        ptr.To(region),
		ServiceName:     ptr.To(endpointServiceName),
		VpcId:           ptr.To(t.Agent.Spec.ACK.VpcID),
		SecurityGroupId: []*string{ptr.To(t.Agent.Spec.ACK.SecurityGroupID)},
		EndpointType:    ptr.To(alicloud.VpcEndpointTypeInterface),
		EndpointName:    ptr.To(endpointName),
		Zone: []*privatelink.CreateVpcEndpointRequestZone{{
			ZoneId:    ptr.To(zoneID),
			VSwitchId: ptr.To(vswitchID),
		}},
		Tag: tagList,
	}

	createResp, err := cli.CreateVpcEndpoint(createReq)
	if err != nil {
		return "", "", errors.Wrap(err, "failed to create vpc endpoint")
	}

	if createResp.Body == nil || createResp.Body.EndpointId == nil {
		return "", "", errors.New("vpc endpoint id not found in response")
	}

	endpointID = *createResp.Body.EndpointId
	if createResp.Body.EndpointDomain == nil {
		return endpointID, "", errors.New("vpc endpoint domain not found in response")
	}

	endpointDomain = *createResp.Body.EndpointDomain
	return endpointID, endpointDomain, nil
}

func (t *PrivateLinkTask) createWithUpbound(ctx context.Context, endpointServiceName string) deploy.Result {
	endpointName, err := alicloudupbound.FindReusableVPCEndpointName(ctx, t.Local, alicloud.VpcEndpointTypeInterface, endpointServiceName, t.Agent.Spec.ACK.VpcID)
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to find reusable vpc endpoint"))
	}
	if endpointName != "" {
		t.L.Info("found an existed endpoint, do not need to create new one", "name", endpointName)
		util.SetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName, endpointName)
		return deploy.Done()
	}

	dnsName := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceOriginDNSName)
	if dnsName == "" {
		return deploy.Error(errors.New("service dns name not found"))
	}
	endpointName = util.InterfaceEndpointName(t.Agent.ObjectMeta, dnsName, consts.O11yEndpointNamePrefixDataplaneNextGen)

	err = t.createInterfaceEndpointUpbound(ctx, endpointServiceName, endpointName)
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to create interface endpoint"))
	}
	t.L.Info("create interface endpoint success", "name", endpointName)

	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName, endpointName)
	return deploy.Done()
}

func (t *PrivateLinkTask) PostCreate(ctx context.Context) deploy.Result {
	if t.Config.IsServerlessMode() {
		return deploy.Done()
	}

	var ep aliplv1alpha1.VpcEndpoint
	endpointName := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName)
	if err := t.Local.Get(ctx, client.ObjectKey{Name: endpointName}, &ep); err != nil {
		return deploy.Error(errors.Wrap(err, "failed to get vpc endpoint"))
	}

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

	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceDNSName, *ep.Status.AtProvider.EndpointDomain)
	return deploy.Done()
}

// Delete removes the Crossplane-managed VPC Interface Endpoint and its zone
// binding. EndpointZone must be removed first to avoid dangling references on
// the provider side.
func (t *PrivateLinkTask) Delete(ctx context.Context) deploy.Result {
	if t.Config.IsServerlessMode() {
		if t.Agent.GetLabels()[consts.LabelKeyPreservePrivateLink] == "true" {
			return deploy.Done()
		}
		return t.deleteWithNativeAPI()
	}
	return t.deleteWithUpbound(ctx)
}

func (t *PrivateLinkTask) deleteWithNativeAPI() deploy.Result {
	endpointID := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName)
	if endpointID == "" {
		return deploy.Done()
	}

	region := t.Config.O11yConfig.Region
	cli, err := t.AliCloud.PrivateLink(nil, alicloud.WithRegion(region))
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to create privatelink client"))
	}

	_, err = cli.DeleteVpcEndpoint(&privatelink.DeleteVpcEndpointRequest{EndpointId: ptr.To(endpointID)})
	if err != nil {
		return deploy.Error(errors.Wrap(err, "failed to delete vpc interface endpoint"))
	}

	t.L.Info("delete interface endpoint success", "endpointID", endpointID)
	util.SetAnnotation(t.Agent, consts.AnnotationKeyO11ySvcInterfaceEndpointName, "")
	return deploy.Done()
}

func (t *PrivateLinkTask) deleteWithUpbound(ctx context.Context) deploy.Result {
	dnsName := util.GetAnnotation(t.Agent, consts.AnnotationKeyO11yServiceOriginDNSName)
	if dnsName == "" {
		return deploy.Done()
	}
	endpointName := util.InterfaceEndpointName(t.Agent.ObjectMeta, dnsName, consts.O11yEndpointNamePrefixDataplaneNextGen)

	epZone := &aliplv1alpha1.VpcEndpointZone{
		ObjectMeta: metav1.ObjectMeta{
			Name: endpointName,
		},
	}
	if err := agentbuild.DeleteObject(ctx, t.Local, epZone); err != nil {
		return deploy.Error(errors.Wrap(err, "failed to delete vpc interface endpoint zone"))
	}

	endpoint := &aliplv1alpha1.VpcEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name: endpointName,
		},
	}
	if err := agentbuild.DeleteObject(ctx, t.Local, endpoint); err != nil {
		return deploy.Error(errors.Wrap(err, "failed to delete vpc interface endpoint"))
	}
	return deploy.Done()
}

// createInterfaceEndpointUpbound materializes two Crossplane resources:
//   - VpcEndpoint: the Interface Endpoint object in the target VPC.
//   - VpcEndpointZone: binds the endpoint to a specific zone and vSwitch.
//
// It deterministically selects a zone from the endpoint service to keep
// reconciliation idempotent.
func (t *PrivateLinkTask) createInterfaceEndpointUpbound(ctx context.Context, endpointServiceName, interfaceEndpointName string) error {
	region := t.Config.O11yConfig.Region
	t.L.Info("creating interface endpoint",
		"endpointServiceName", endpointServiceName,
		"interfaceEndpointName", interfaceEndpointName,
		"region", region)
	if region == "" {
		return errors.New("AliCloud region is not configured")
	}

	chosenZone, vswitchID, err := t.findZoneAndVSwitch(endpointServiceName, true)
	if err != nil {
		return err
	}

	endpoint := &aliplv1alpha1.VpcEndpoint{
		ObjectMeta: metav1.ObjectMeta{
			Name: interfaceEndpointName,
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, t.Local, endpoint, func() error {
		endpoint.Spec.ProviderConfigReference = t.Agent.Spec.ACK.ProviderConfigReference
		endpoint.Spec.ForProvider.Region = ptr.To(region)
		endpoint.Spec.ForProvider.ServiceName = ptr.To(endpointServiceName)
		endpoint.Spec.ForProvider.VPCID = ptr.To(t.Agent.Spec.ACK.VpcID)
		if t.Agent.Spec.ACK.SecurityGroupID == "" {
			return errors.New("security group id is empty")
		}
		endpoint.Spec.ForProvider.SecurityGroupIds = []*string{ptr.To(t.Agent.Spec.ACK.SecurityGroupID)}
		endpoint.Spec.ForProvider.EndpointType = ptr.To(alicloud.VpcEndpointTypeInterface)
		endpoint.Spec.ForProvider.Tags = t.buildVPCEndpointTags(interfaceEndpointName)
		if endpoint.Labels == nil {
			endpoint.Labels = make(map[string]string)
		}
		endpoint.Labels[consts.LabelKeyRelatedCluster] = t.Agent.Spec.KubernetesClusterName
		endpoint.Labels[agentv2.TagKeyTenant] = util.GetLabel(t.Agent, agentv2.TagKeyTenant)
		return nil
	})
	if err != nil {
		return errors.Wrap(err, "failed to create/update interface endpoint resource")
	}

	epZone := &aliplv1alpha1.VpcEndpointZone{
		ObjectMeta: metav1.ObjectMeta{
			Name: interfaceEndpointName,
		},
	}
	_, err = controllerutil.CreateOrUpdate(ctx, t.Local, epZone, func() error {
		epZone.Spec.ProviderConfigReference = t.Agent.Spec.ACK.ProviderConfigReference
		epZone.Spec.ForProvider.Region = ptr.To(region)
		epZone.Spec.ForProvider.ZoneID = ptr.To(chosenZone)
		epZone.Spec.ForProvider.EndpointIDRef = &xpv1.Reference{Name: interfaceEndpointName}
		epZone.Spec.ForProvider.VswitchID = ptr.To(vswitchID)
		return nil
	})
	if err != nil {
		return errors.Wrap(err, "failed to create/update interface endpoint zone resource")
	}

	return nil
}
