package gardener

import (
	"context"
	"fmt"
	"time"

	awsutil "github.com/aws/aws-sdk-go-v2/aws"
	"github.com/pingcap/errors"
	"go.opentelemetry.io/otel/trace"
	corev1 "k8s.io/api/core/v1"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"

	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	reconcileerr "github.com/tidbcloud/infra-api/controllers/common/error"
	"github.com/tidbcloud/infra-api/pkg/config"
	"github.com/tidbcloud/infra-api/pkg/consts"
	gardencorev1beta1 "github.com/tidbcloud/infra-api/pkg/gardener/core/apis/v1beta1"
	"github.com/tidbcloud/infra-api/pkg/o11y"
	"github.com/tidbcloud/infra-api/pkg/o11y/log_agent"
	"github.com/tidbcloud/infra-api/pkg/o11y/metric_agent"
	tracerutil "github.com/tidbcloud/infra-api/pkg/tracer"
	"github.com/tidbcloud/infra-api/pkg/vpc_endpoint"
)

const (
	vectorChartRelativePath = "/charts/vector.tgz"
	// FIXME(@xuyifan02): typo, should be "o11y-vector". Since it is already in production, we can not change it now.
	vectorReleaseName = "o11y-vmagent"
	loggingNamespace  = "logging"
	fakeClusterID     = "k8s-infra"

	// FIXME(@xuyifan02): The Network.Status.IAM.AccountID is empty in runtime. This cause GetRootUserArn to return invalid principle arn arn:aws:iam:::root.
	// Currently, we can force the parameter to empty string.
	agentManagerARN = ""
)

var (
	defaultWaitInterval = 10 * time.Second
)

func (r *NetworkReconciler) ensureLogAgent(
	ctx context.Context,
	network *cloudv1.Network,
	shootKubeConfig []byte,
	shoot *gardencorev1beta1.Shoot,
	networkGardenStatus *cloudv1.GardenerNetworkStatus,
	providerGardenerSpec *cloudv1.GardenerProviderSpec,
	shootClient client.Client,
) (op controllerutil.OperationResult, errResult error) {
	var span trace.Span
	ctx, span = tracer.Start(ctx, "create shoot k8s log agent")
	defer func() { tracerutil.EndSpan(span, errResult) }()

	tenantID, projectID := log_agent.GetIDsFromNetworkCRD(network)

	// Create o11y infra via autonomous global openapi.
	o11yCluster, err := r.o11yApi.CreateAwsO11yInfra(ctx, networkGardenStatus.NodeRole, agentManagerARN, network.Spec.Provider, network.Spec.Region, tenantID, projectID, fakeClusterID, o11y.SHARED)
	if err != nil {
		return controllerutil.OperationResultNone, reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to create o11y infra"))
	}

	// create private endpoint to the regional loki storage
	if err := r.ensurePrivateEndpoint(ctx, o11yCluster, network, shoot, networkGardenStatus, providerGardenerSpec); err != nil {
		return controllerutil.OperationResultNone, errors.Annotate(err, "failed to ensure private endpoint")
	}

	bucket, err := log_agent.GetLogStorageInfo(o11yCluster.StorageInfo)
	if err != nil {
		return controllerutil.OperationResultNone, errors.Annotate(err, "failed to get log storage bucket name")
	}

	if err := log_agent.EnsureLoggingNamespace(ctx, shootClient); err != nil {
		return controllerutil.OperationResultNone, errors.Annotate(err, "failed to ensure logging namespace")
	}

	if err := log_agent.EnsureAgentPriorityClass(ctx, shootClient); err != nil {
		return controllerutil.OperationResultNone, errors.Annotate(err, "failed to ensure agent priority class")
	}

	lokiURLBuilder := metric_agent.NewWriteURLBuilder(*o11yCluster.MonitorInfo.DnsName)
	var chartValues []byte
	switch network.Spec.Provider {
	case consts.ProviderTypeAWS:
		chartValues = log_agent.BuildAwsDefaultVectorChartValuesForShoot(
			r.env.ProjectName(),
			network.Spec.Region,
			tenantID,
			projectID,
			consts.ProviderTypeAWS,
			shoot.Status.TechnicalID,
			bucket,
			o11yCluster.AgentInfo.RoleARN,
			lokiURLBuilder.ForAWS().BuildLoki(),
			config.GetVectorForLogRepo(),
			config.VectorForLogVersion,
		)
	case consts.ProviderTypeGCP:
		dnsName := *o11yCluster.MonitorInfo.DnsName
		endpointName := metric_agent.InterfaceEndpointName(dnsName, shoot.Name)
		chartValues = log_agent.BuildGcpDefaultVectorChartValuesForShoot(
			r.env.ProjectName(),
			tenantID,
			projectID,
			consts.ProviderTypeGCP,
			shoot.Status.TechnicalID,
			bucket,
			lokiURLBuilder.ForGCP(endpointName).BuildLoki(),
			config.GetVectorForLogRepo(),
			config.VectorForLogVersion,
		)
	default:
		return controllerutil.OperationResultNone, errors.Annotate(err, "unsupported provider type")
	}

	_, _, err = r.o11yApi.InstallAgentWithHelm(r.manifestDirectoryPrefix, vectorChartRelativePath, vectorReleaseName, loggingNamespace, shoot.Spec.Kubernetes.Version, shootKubeConfig, chartValues)
	if err != nil {
		return controllerutil.OperationResultNone, reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to install shoot log agent"))
	}

	log_agent.AddO11yIDToNetwork(network, o11yCluster.ID)

	if err := r.Update(ctx, network); err != nil {
		return controllerutil.OperationResultNone, reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to update network crd"))
	}

	r.record.Event(network, corev1.EventTypeNormal, "ensureLogAgent", "Ensure o11y log agent successful")
	return op, nil
}

func (r *NetworkReconciler) cleanupLogAgent(ctx context.Context, network *cloudv1.Network, o11yApi o11y.Api) (errResult error) {
	_, span := tracer.Start(ctx, "delete shoot log agent")
	defer func() { tracerutil.EndSpan(span, errResult) }()

	o11yID, ok := network.Annotations[consts.O11yIDAnnotationKey]
	if ok {
		// If o11yID annotation is not empty, delete o11y infra.
		if _, err := o11yApi.DeleteO11yInfra(ctx, network.Spec.Provider, network.Spec.Region, o11yID); err != nil {
			return reconcileerr.New(defaultWaitInterval, err)
		}
	} else {
		// Else o11yID is empty, try to fetch o11y id from api, then delete o11y infra.
		tenantID, projectID := log_agent.GetIDsFromNetworkCRD(network)
		o11yInfra, err := r.o11yApi.GetO11yInfra(ctx, network.Spec.Provider, network.Spec.Region, tenantID, projectID, fakeClusterID)
		if err != nil {
			// Retry if api returns non 200 error.
			return reconcileerr.New(defaultWaitInterval, err)
		}
		if len(o11yInfra) == 1 {
			// Only if exactly one o11y infra is found, delete o11y infra.
			if _, err := o11yApi.DeleteO11yInfra(ctx, network.Spec.Provider, network.Spec.Region, o11yInfra[0].ID); err != nil {
				return reconcileerr.New(defaultWaitInterval, err)
			}
		} else {
			// If err is nil and len(o11yInfra) is not 1, do nothing but log error.
			log.FromContext(ctx).Info("GetO11yInfra returns zero or more than one o11y infra, should be exactly one. This could means that the o11y cluster is already deleted.")
		}
	}

	if err := r.deletePrivateEndpoint(ctx); err != nil {
		return reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to delete private endpoint"))
	}

	r.record.Event(network, corev1.EventTypeNormal, "cleanupLogAgent", "cleanup o11y log agent successful")
	return nil
}

// Won't do anything. Endpoints are deleted on network deletion or hibernation.
func (r *NetworkReconciler) deletePrivateEndpoint(_ context.Context) error {
	return nil
}

// ensurePrivateEndpoint create an interface endpoint in aws, or a published service psc endpoint in gcp.
//
// Hack: Mostly ported from github.com/tidbcloud/infra-provider/infra-api/pkg/o11y/metric_agent.ensurePrivateEndpoint
//
//	An interface endpoint/psc endpoint is needed to send logs to loki storage. We reuse the code because they
//	transmit in the same way to the regional vpc but at not the same stage.
func (r *NetworkReconciler) ensurePrivateEndpoint(
	ctx context.Context,
	o11yCluster *o11y.ObservabilityCluster,
	network *cloudv1.Network,
	shoot *gardencorev1beta1.Shoot,
	networkGardenStatus *cloudv1.GardenerNetworkStatus,
	providerGardenerSpec *cloudv1.GardenerProviderSpec,
) (errResult error) {
	if !vpc_endpoint.ShouldUpdateEndpoint(network.Annotations, r.o11yConfig.Version) {
		return nil
	}

	// Check endpoint service name and dns name correct.
	if o11yCluster.MonitorInfo.DnsName == nil || len(*o11yCluster.MonitorInfo.DnsName) <= 0 || len(o11yCluster.MonitorInfo.EndpointServiceName) <= 0 {
		return errors.New("failed to create o11y infra, dns name or endpoint service name not ready. retry later")
	}

	switch network.Spec.Provider {
	case consts.ProviderTypeAWS:
		sgs := make([]*string, 0, len(networkGardenStatus.AWS.SecurityGroups))
		for _, sg := range networkGardenStatus.AWS.SecurityGroups {
			sgs = append(sgs, awsutil.String(sg.ID))
		}

		_, err := metric_agent.CreateInterfaceEndpointForMetricStorage(
			ctx,
			r.Client,
			r.awsClient.Ec2(),
			*networkGardenStatus.AWS.VpcID,
			shoot.Status.TechnicalID,
			shoot.Name,
			o11yCluster.MonitorInfo.EndpointServiceName,
			network.Spec.Region,
			*o11yCluster.MonitorInfo.DnsName,
			providerGardenerSpec.AWS.CrossplaneProvider,
			network.Labels,
			sgs,
		)
		if err != nil {
			return reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to create interface endpoint to the regional loki storage"))
		}

	case consts.ProviderTypeGCP:
		subnetString := fmt.Sprintf("%s-internal", shoot.Status.TechnicalID)
		err := r.gcpapi.CreatePublishedServicePsc(
			ctx,
			providerGardenerSpec.GCP.ProjectID,
			network.Spec.Region,
			*networkGardenStatus.GCP.NetworkName,
			subnetString,
			o11yCluster.MonitorInfo.EndpointServiceName,
			metric_agent.InterfaceEndpointName(*o11yCluster.MonitorInfo.DnsName, shoot.Name),
			metric_agent.DomainNamespace(*o11yCluster.MonitorInfo.DnsName),
		)
		if err != nil {
			return reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to create published service psc to the regional loki storage"))
		}

	default:
		return errors.Errorf("unsupported provider type: %s", network.Spec.Provider)
	}

	endpointName := metric_agent.InterfaceEndpointName(*o11yCluster.MonitorInfo.DnsName, shoot.Name)
	metric_agent.AddEndpointNameToNetworkAnn(network, network.Name, endpointName)
	return nil
}
