package gardener

import (
	"context"
	"time"

	"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"
	tracerutil "github.com/tidbcloud/infra-api/pkg/tracer"
)

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, shootNodeRole string, 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, shootNodeRole, 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"))
	}

	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")
	}

	var chartValues []byte
	switch network.Spec.Provider {
	case consts.ProviderTypeAWS:
		chartValues = log_agent.BuildAwsDefaultVectorChartValuesForShoot(network.Spec.Region, tenantID, projectID, shoot.Status.TechnicalID, bucket, o11yCluster.AgentInfo.RoleARN, config.GetVectorRepo(), config.VectorVersion)
	case consts.ProviderTypeGCP:
		chartValues = log_agent.BuildGcpDefaultVectorChartValuesForShoot(tenantID, projectID, shoot.Status.TechnicalID, bucket, config.GetVectorRepo(), config.VectorVersion)
	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.")
		}
	}

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