package cluster

import (
	"context"
	"fmt"

	awsutil "github.com/aws/aws-sdk-go-v2/aws"
	"github.com/pingcap/errors"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/multierr"
	appv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

	reconcileerr "github.com/tidbcloud/infra-api/controllers/common/error"
	"github.com/tidbcloud/infra-api/pkg/config"
	"github.com/tidbcloud/infra-api/pkg/consts"
	"github.com/tidbcloud/infra-api/pkg/o11y"
	"github.com/tidbcloud/infra-api/pkg/o11y/metric_agent"
	"github.com/tidbcloud/infra-api/pkg/o11y/vector_agent"
	tracerutil "github.com/tidbcloud/infra-api/pkg/tracer"
	"github.com/tidbcloud/infra-api/pkg/vpc_endpoint"
)

const (
	agentStorageClassName = "o11y-metric-agent"

	vmagentChartRelativePath = "/charts/victoria_metrics_agent.tgz"
	vmagentReleaseName       = "o11y-vmagent"

	prometheusConfigMapName = "db-monitor"

	vmagentReleaseFullName = "o11y-vmagent-victoria-metrics-agent"

	vectorAgentFullName = "o11y-vector-agent"

	// 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 = ""
)

// Cluster agent is the replacement of db-monitor in TiDB cluster.
func (r *reconcileContext) ensureClusterAgent(ctx context.Context) (errResult error) {
	var span trace.Span
	ctx, span = tracer.Start(ctx, "create cluster agent")
	defer func() { tracerutil.EndSpan(span, errResult) }()

	if err := metric_agent.EnsureAgentStorageClass(ctx, r.shootClient, r.Network.Spec.Provider); err != nil {
		return errors.Annotate(err, "failed to ensure storage class")
	}

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

	// Create o11y infra via autonomous global openapi.
	tenantID, projectID, clusterID := metric_agent.GetIDsFromClusterCRD(r.Target, r.Network.Name)
	o11yCluster, err := r.o11yApi.CreateAwsO11yInfra(ctx, r.networkGardenStatus.NodeRole, agentManagerARN, r.Network.Spec.Provider, r.Network.Spec.Region, tenantID, projectID, clusterID, o11y.EXCLUSIVE)
	if err != nil {
		return errors.Annotate(err, "failed to create o11y infra")
	}

	// Different clusters' endpoint will be reconciled automatically even if they have the same endpoint service name.
	if err := r.ensurePrivateEndpoint(ctx, o11yCluster); err != nil {
		return err
	}

	// Create vmagent. The vmagent will replace prometheus(db-monitor) in the future.
	// FIXME(@xuyifan02): scrape targets should controlled by infra-provider, but not borrowed from prometheus by mount its configmap.
	//                    Currently implementation is just a lazy hack.
	// Get prometheus configmap.
	promConfig, err := metric_agent.GetPromConfig(ctx, r.shootClient, r.Target.PrefixedId(), r.shoot.Status.TechnicalID)
	if err != nil {
		return reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to get prometheus configmap, maybe not ready"))
	}
	chartValues, err := metric_agent.BuildDefaultVmagentChartValuesForTidbCluster(config.GetVmagentRepo(), config.VmagentVersion, agentStorageClassName, promConfig, 1, r.buildRemoteWriteURL(o11yCluster))
	if err != nil {
		return errors.Annotate(err, "failed to build default vmagent chart values")
	}
	_, _, err = r.o11yApi.InstallAgentWithHelm(r.manifestDirectoryPrefix, vmagentChartRelativePath, vmagentReleaseName, r.Target.PrefixedId(), r.shoot.Spec.Kubernetes.Version, r.shootKubeConfig, chartValues)
	if err != nil {
		return reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to install o11y cluster agent"))
	}

	// Since vmagent helm chart has limited support for rabc, we need to override the clusterrolebinding.
	if err := metric_agent.EnsureAgentClusterRoleBinding(ctx, r.shootClient, vmagentReleaseFullName, r.Target.PrefixedId()); err != nil {
		return reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to ensure clusterrolebinding"))
	}

	if err := metric_agent.UpdateControlAnnotation(ctx, r.Client, o11yCluster, r.Target, r.Network, r.shoot.Name, r.o11yConfig.Version); err != nil {
		return reconcileerr.New(defaultWaitInterval, errors.Annotate(err, "failed to update cluster crd"))
	}

	// Deploy Cluster Vector Agent
	cmName := vectorAgentFullName
	pvcName := vectorAgentFullName
	deployName := vectorAgentFullName

	if err := vector_agent.CreateConfigMap(ctx, r.shootClient, cmName, r.Target.PrefixedId(), r.buildVMImportURL(o11yCluster)); err != nil {
		return errors.Annotate(err, "failed to create vector agent configmap")
	}

	if err := vector_agent.CreatePVC(ctx, r.shootClient, pvcName, r.Target.PrefixedId(), agentStorageClassName); err != nil {
		return errors.Annotate(err, "failed to create vector agent pvc")
	}

	vectorEnvLabels := vector_agent.BuildEnvLabels(r.shoot, r.Target, r.Network)
	if err := vector_agent.CreateDeployment(ctx, r.shootClient, deployName, cmName, pvcName, r.Target.PrefixedId(), config.GetVectorForClusterImage(), 1, vectorEnvLabels); err != nil {
		return errors.Annotate(err, "failed to create vector agent deployment")
	}

	r.recorder.Event(r.Target, corev1.EventTypeNormal, "ensureClusterAgent", "ensure o11y cluster agent successful")
	return nil
}

// deleteClusterAgent call o11y openapi to cleanup resource in o11y vpc.
func (r *reconcileContext) deleteClusterAgent(ctx context.Context) (errResult error) {
	_, span := tracer.Start(ctx, "delete cluster agent")
	defer func() { tracerutil.EndSpan(span, errResult) }()

	var errs error

	// Delete vector agent deployment.
	err := r.shootClient.Delete(r.ctx, &appv1.Deployment{ObjectMeta: metav1.ObjectMeta{
		Name:      vectorAgentFullName,
		Namespace: r.Target.PrefixedId(),
	}})
	if err != nil && !apierrors.IsNotFound(err) {
		errs = multierr.Append(errs, errors.Annotate(err, "failed to delete vector agent deployment"))
	}

	// Delete vector agent pvc
	err = r.shootClient.Delete(r.ctx, &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{
		Name:      vectorAgentFullName,
		Namespace: r.Target.PrefixedId(),
	}})
	if err != nil && !apierrors.IsNotFound(err) {
		errs = multierr.Append(errs, errors.Annotate(err, "failed to delete vector agent pvc"))
	}

	// Delete vmagent deployment.
	err = r.shootClient.Delete(r.ctx, &appv1.Deployment{ObjectMeta: metav1.ObjectMeta{
		Name:      vmagentReleaseFullName,
		Namespace: r.Target.PrefixedId(),
	}})
	if err != nil && !apierrors.IsNotFound(err) {
		errs = multierr.Append(errs, errors.Annotate(err, "failed to delete vmagent deployment"))
	}

	// Delete vmagent pvc
	err = r.shootClient.Delete(r.ctx, &corev1.PersistentVolumeClaim{ObjectMeta: metav1.ObjectMeta{
		Name:      vmagentReleaseFullName,
		Namespace: r.Target.PrefixedId(),
	}})
	if err != nil && !apierrors.IsNotFound(err) {
		errs = multierr.Append(errs, errors.Annotate(err, "failed to delete vmagent pvc"))
	}

	o11yID, ok := r.Target.Annotations[consts.O11yIDAnnotationKey]
	if ok {
		// If o11yID annotation is not empty, delete o11y infra.
		if _, err := r.o11yApi.DeleteO11yInfra(ctx, r.Network.Spec.Provider, r.Network.Spec.Region, o11yID); err != nil {
			errs = multierr.Append(errs, errors.Annotate(err, "failed to delete o11y infra"))
		}
	} else {
		// Else o11yID is empty, try to fetch o11y id from api.
		tenantID, projectID, clusterID := metric_agent.GetIDsFromClusterCRD(r.Target, r.Network.Name)
		o11yInfra, err := r.o11yApi.GetO11yInfra(ctx, r.Network.Spec.Provider, r.Network.Spec.Region, tenantID, projectID, clusterID)
		if err != nil {
			// Retry if api returns non 200 error.
			errs = multierr.Append(errs, errors.Annotate(err, "failed to get o11y infra"))
		} else if len(o11yInfra) == 1 {
			// Only if exactly one o11y infra is found, delete o11y infra.
			if _, err := r.o11yApi.DeleteO11yInfra(ctx, r.Network.Spec.Provider, r.Network.Spec.Region, o11yInfra[0].ID); err != nil {
				errs = multierr.Append(errs, errors.Annotate(err, "failed to delete o11y infra"))
			}
		} else {
			// If err is nil and len(o11yInfra) is not 1, do nothing but log error.
			r.Log.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 {
		errs = multierr.Append(errs, errors.Annotate(err, "failed to delete private endpoint"))
	}

	if errs != nil {
		return reconcileerr.New(defaultWaitInterval, errs)
	}

	r.recorder.Event(r.Target, corev1.EventTypeNormal, "deleteClusterAgent", "cleanup o11y cluster agent successful")
	return nil
}

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

// ensurePrivateEndpoint create an interface endpoint in aws, or an published service psc endpoint in gcp.
func (r *reconcileContext) ensurePrivateEndpoint(ctx context.Context, o11yCluster *o11y.ObservabilityCluster) (errResult error) {
	if !vpc_endpoint.ShouldUpdateEndpoint(r.Target.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, metric write dns name or endpoint service name not ready. retry later")
	}

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

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

	case consts.ProviderTypeGCP:
		subnetString := fmt.Sprintf("%s-internal", r.shoot.Status.TechnicalID)
		err := r.gcpApi.CreatePublishedServicePsc(
			ctx,
			r.providerGardenerSpec.GCP.ProjectID,
			r.Network.Spec.Region,
			*r.networkGardenStatus.GCP.NetworkName,
			subnetString,
			o11yCluster.MonitorInfo.EndpointServiceName,
			metric_agent.InterfaceEndpointName(*o11yCluster.MonitorInfo.DnsName, r.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 metric remote write storage"))
		}

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

	return nil
}

func (r *reconcileContext) buildRemoteWriteURL(o11yCluster *o11y.ObservabilityCluster) (url string) {
	b := metric_agent.NewWriteURLBuilder(*o11yCluster.MonitorInfo.DnsName)
	switch r.Network.Spec.Provider {
	case consts.ProviderTypeAWS:
		url = b.ForAWS().BuildRemoteWrite(o11yCluster.ID)
	case consts.ProviderTypeGCP:
		endpointName := metric_agent.InterfaceEndpointName(*o11yCluster.MonitorInfo.DnsName, r.shoot.Name)
		url = b.ForGCP(endpointName).BuildRemoteWrite(o11yCluster.ID)
	}
	return
}

func (r *reconcileContext) buildVMImportURL(o11yCluster *o11y.ObservabilityCluster) (url string) {
	b := metric_agent.NewWriteURLBuilder(*o11yCluster.MonitorInfo.DnsName)
	switch r.Network.Spec.Provider {
	case consts.ProviderTypeAWS:
		url = b.ForAWS().BuildVMImport()
	case consts.ProviderTypeGCP:
		endpointName := metric_agent.InterfaceEndpointName(*o11yCluster.MonitorInfo.DnsName, r.shoot.Name)
		url = b.ForGCP(endpointName).BuildVMImport()
	}
	return
}
