package audit

import (
	"context"
	"fmt"
	"path/filepath"
	"strconv"
	"time"

	"github.com/Masterminds/semver"
	"github.com/pingcap/errors"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	apps "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/apimachinery/pkg/util/sets"
	"k8s.io/utils/pointer"
	ctrl "sigs.k8s.io/controller-runtime"
	"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"
	"github.com/tidbcloud/infra-api/pkg/audit/server"
	"github.com/tidbcloud/infra-api/pkg/config"
	"github.com/tidbcloud/infra-api/pkg/consts"
)

var auditLogVersionConstraint, _ = semver.NewConstraint(">= v5.4.0")

const metricsPortAnnotationKey = "tidb.auditlog.prometheus.io/port"
const metricsPort = 9090

func RegisterGCPCredentialGetter(provider string, fn CredentialGetter) {
	gcpCredentials[provider] = fn
}

type CredentialGetter func(ctx context.Context, cli client.Client, network *cloudv1.Network) ([]byte, error)

var gcpCredentials = make(map[string]CredentialGetter)

var provider2CloudProvider = map[string]string{
	consts.ProviderTypeAWS: cloudv1.CloudProviderAWS,
	consts.ProviderTypeEKS: cloudv1.CloudProviderAWS,
	consts.ProviderTypeGCP: cloudv1.CloudProviderGCP,
	consts.ProviderTypeGKE: cloudv1.CloudProviderGCP,
}

// Provider2VolumnTags support setting tags to the volume(aws only) with specific provider.
var Provider2VolumnTags = make(map[string]map[string]string)

// SidecarTemplate used to build the sidecar.
// Only the flowing field will be used: Image, Command, Resources
var SidecarTemplate = corev1.Container{
	Image: config.GetBaseImage("busybox:1.31.1"),
	Command: []string{
		"sh",
		"-c",
		"touch /var/log/auditlog/tidb-audit.log; tail -n0 -F /var/log/auditlog/tidb-audit.log;",
	},
	// DO NOT specify request to increase the resource need by the pod.
	// Generally this sidecar should take only a pretty small resource, specify limits to avoid
	// affect the tidb pod when something unexpected take a lot resource.
	Resources: corev1.ResourceRequirements{
		Limits: corev1.ResourceList{
			corev1.ResourceCPU:    resource.MustParse("100m"),
			corev1.ResourceMemory: resource.MustParse("256Mi"),
		},
		Requests: corev1.ResourceList{
			corev1.ResourceCPU:    resource.MustParse("0m"),
			corev1.ResourceMemory: resource.MustParse("0Mi"),
		},
	},
}

const (
	auditLogName             = "auditlog"
	mountPath                = "/var/log/auditlog"
	auditLogStorageClassName = "auditlog"
	configFilePath           = "/etc/audit/audit.yaml"

	pluginName = "audit-1"

	// Note the slowlog file(--log-slow-query) parameter of tidb will be filepath.Join(mountPath, slowlogVolumeName)
	// see https://github.com/pingcap/tidb-operator/issues/4396 for detail.
	slowlogVolumeName = "slowlog"
)

func ensureAuditLogStorageClass(ctx context.Context, cli client.Client, network *cloudv1.Network) error {
	cloudProvider, ok := provider2CloudProvider[network.Spec.Provider]
	if !ok {
		return errors.Errorf("unknown provider %q", network.Spec.Provider)
	}

	tags := Provider2VolumnTags[network.Spec.Provider]

	switch cloudProvider {
	case cloudv1.CloudProviderAWS:
		return ensureAWSStorageClass(ctx, cli, tags)
	case cloudv1.CloudProviderGCP:
		return ensureGCPStorageClass(ctx, cli)
	default:
		return errors.Errorf("unknown cloud provider %q", cloudProvider)
	}
}

func ensureAWSStorageClass(ctx context.Context, cli client.Client, tags map[string]string) error {
	// ref: https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/TAGGING.md
	params := map[string]string{
		"type": "gp3",
	}

	for k, v := range tags {
		params["tagSpecification_"+k] = fmt.Sprintf("%s=%s", k, v)
	}

	err := cloudv1.EnsureAWSStorageClass(ctx, cli, auditLogStorageClassName, params)

	return err
}

func ensureGCPStorageClass(ctx context.Context, cli client.Client) error {
	err := cloudv1.EnsureGCPStorageClass(ctx, cli, auditLogStorageClassName, map[string]string{
		"type": "pd-ssd",
	})
	return err
}

func buildAuditLogStorageVolume(audit *cloudv1.Audit) v1alpha1.StorageVolume {
	return v1alpha1.StorageVolume{
		Name:             slowlogVolumeName,
		StorageClassName: pointer.String(auditLogStorageClassName),
		StorageSize:      "30Gi",
		MountPath:        mountPath,
	}
}

func buildSidecar(configMapName string) corev1.Container {
	tpl := SidecarTemplate
	return corev1.Container{
		Name:      auditLogName,
		Image:     tpl.Image,
		Command:   tpl.Command,
		Resources: tpl.Resources,
		VolumeMounts: []corev1.VolumeMount{
			{
				Name:      "tidb-" + slowlogVolumeName,
				MountPath: mountPath,
			},
			{
				Name:      configVolumeName,
				MountPath: configMountPath(),
			},
		},
		Env: []corev1.EnvVar{
			// For the sidecar to get pod slot(ordinal) for append to the url suffix.
			// Note all pod use the same configmap as config file.
			{
				Name: "POD_NAME",
				ValueFrom: &corev1.EnvVarSource{
					FieldRef: &corev1.ObjectFieldSelector{
						FieldPath: "metadata.name",
					},
				},
			},
		},
	}
}

func configMapName(cluster *cloudv1.Cluster) string {
	return cluster.Name + "-audit"
}

var configVolumeName = "audit-config"

func buildConfigMapVolume(configMapName string) corev1.Volume {
	return corev1.Volume{
		Name: configVolumeName,
		VolumeSource: corev1.VolumeSource{
			ConfigMap: &corev1.ConfigMapVolumeSource{
				LocalObjectReference: corev1.LocalObjectReference{
					Name: configMapName,
				},
			},
		},
	}
}

func configMountPath() string {
	return filepath.Dir(configFilePath)
}

func createOrUpdateConfigMap(
	ctx context.Context,
	network *cloudv1.Network,
	cluster *cloudv1.Cluster,
	tc *v1alpha1.TidbCluster,
	tcClient client.Client,
) error {
	a := cluster.Spec.Cluster.TiDB.Audit
	cfg := server.Config{
		LogDir:                mountPath,
		BucketURL:             a.BucketURL,
		Region:                a.Region,
		RoleARN:               a.RoleArn,
		ExternalID:            getExternalID(network, cluster),
		SlowLogMaxBackups:     1,
		CleanOldestLogPercent: 0.8,
		// This assume after gracefulStopWaitSeconds tidb will stop writing the audit file.
		// and we will rotate the current writing file ourself and push it.
		// tidb will force closing all connection after 15s.
		// ref https://github.com/pingcap/tidb/blob/master/server/server.go#L718
		//
		// the default terminationGracePeriodSeconds of pod is 30
		// the max size of audit log file is 30M so in normal it will enough for us to push the file.
		// one test for reference:
		// [name=/var/log/auditlog/tidb-audit-2022-02-07T13-09-09.222.log] [size=104857600] [takes=6.447067187s]]]
		// manual generate a 100M file and it take 6 second to push it.
		GracefulStopWaitSeconds: 18,
		MetricsPort:             metricsPort,
	}
	if tcfg := tc.Spec.TiDB.Config; tcfg != nil {
		v := tcfg.Get("graceful-wait-before-shutdown")
		if v != nil {
			val, err := v.AsInt()
			if err == nil {
				if newwait := int(val + 18); newwait > cfg.GracefulStopWaitSeconds {
					cfg.GracefulStopWaitSeconds = newwait
				}
			}
		}
	}

	data, err := cfg.ToYAML()
	if err != nil {
		return err
	}

	cm := &corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: tc.Namespace,
			Name:      configMapName(cluster),
		},
	}

	mp := map[string]string{
		"audit.yaml": string(data),
	}

	_, err = controllerutil.CreateOrUpdate(ctx, tcClient, cm, func() error {
		cm.Data = mp
		return nil
	})

	if err != nil {
		return errors.Annotate(err, "failed to create or update configmap")
	}

	return nil
}

var recheckDuration = time.Second * 5

// CheckIfSupported checks if the given TiDB version supports the audit log feature.
func CheckIfSupported(ctx context.Context, v string) bool {
	logger := log.FromContext(ctx)

	version, err := semver.NewVersion(v)
	if err != nil {
		logger.Error(err, "failed to parse tidb version", "version", v)
		return false
	}

	return auditLogVersionConstraint.Check(version)
}

func onCreate(
	ctx context.Context,
	network *cloudv1.Network,
	cluster *cloudv1.Cluster,
	infraClient client.Client,
	tc *v1alpha1.TidbCluster,
	tcClient client.Client,
) (ctrl.Result, error) {
	audit := cluster.Spec.Cluster.TiDB.Audit
	if audit == nil || audit.Enable == false {
		return ctrl.Result{}, nil
	}

	err := ensureAuditLogStorageClass(ctx, tcClient, network)
	if err != nil {
		return ctrl.Result{}, err
	}

	err = createOrUpdateConfigMap(ctx, network, cluster, tc, tcClient)
	if err != nil {
		return ctrl.Result{}, err
	}

	if metricsPort != 0 {
		if tc.Spec.TiDB.Annotations == nil {
			tc.Spec.TiDB.Annotations = make(map[string]string)
		}
		tc.Spec.TiDB.Annotations[metricsPortAnnotationKey] = strconv.Itoa(metricsPort)
	}

	storageVolume := buildAuditLogStorageVolume(audit)
	sidecar := buildSidecar(configMapName(cluster))
	volume := buildConfigMapVolume(configMapName(cluster))

	err = setupGCPCredentials(ctx, network, cluster, infraClient, tc, tcClient, &sidecar)
	if err != nil {
		return ctrl.Result{}, err
	}

	tc.Spec.TiDB.SlowLogVolumeName = storageVolume.Name
	tc.Spec.TiDB.StorageVolumes = addStorageVolumeIfNotExist(tc.Spec.TiDB.StorageVolumes, storageVolume)
	tc.Spec.TiDB.AdditionalContainers = addContainerIfNotExist(tc.Spec.TiDB.AdditionalContainers, sidecar)
	tc.Spec.TiDB.Plugins = addStringIfNotExist(tc.Spec.TiDB.Plugins, pluginName)
	tc.Spec.TiDB.AdditionalVolumes = addVolumeIfNotExist(tc.Spec.TiDB.AdditionalVolumes, volume)

	return ctrl.Result{}, nil
}

// Reconcile will create a storage class and mount a mount to tidb.
// The PVC will not be delete when cluster deleted, user need to delete it.
// tc.Spec.TiDB.SlowLogVolumeName will be set as empty if audit log is not enable.
//
// When enable will:
//  1. Create StorageClass, and configmap.
//  2. Update tc:
//     a. Add audit plugin into `spec.TiDB.Plugins`
//     b. Add sidecar into `Spec.TiDB.AdditionalContainers`
//     c. Set `Spec.TiDB.SlowLogVolumeName`
//     d. Add item into `tc.Spec.TiDB.StorageVolumes` to mount a volume using the created at step 1.
//  3. Make sure sts updated. Note `volumeClaimTemplates` of StatefulSet do not support updated, and operator will ignore it when updating STS, see: https://github.com/pingcap/tidb-operator/pull/4369. to address this:
//
// we will:
//
//	a. Delete tidb sts with DeletePropagationOrphan option to make operator recreate STS according the newest tc spec.
//	b. Delete the pods starting from slot 0 one by one to make sts recreate it. STS controller will fail to update the pod since it try to update `Volumes` field of the pod and failed.
func Reconcile(
	ctx context.Context,
	network *cloudv1.Network,
	cluster *cloudv1.Cluster,
	infraClient client.Client,
	tc *v1alpha1.TidbCluster,
	tcClient client.Client,
	advanceSTS bool,
) (ctrl.Result, error) {
	if tc.CreationTimestamp.IsZero() {
		return onCreate(ctx, network, cluster, infraClient, tc, tcClient)
	}

	logger := log.FromContext(ctx)

	audit := cluster.Spec.Cluster.TiDB.Audit
	storageVolume := buildAuditLogStorageVolume(audit)
	sidecar := buildSidecar(configMapName(cluster))
	volume := buildConfigMapVolume(configMapName(cluster))

	oldTC := tc.DeepCopy()

	if audit != nil && audit.Enable {
		err := ensureAuditLogStorageClass(ctx, tcClient, network)
		if err != nil {
			return ctrl.Result{}, err
		}

		// Note mounted-configmaps-are-updated-automatically, the application can detect it and apply the new config.
		// https://kubernetes.io/docs/concepts/configuration/configmap/#mounted-configmaps-are-updated-automatically
		err = createOrUpdateConfigMap(ctx, network, cluster, tc, tcClient)
		if err != nil {
			return ctrl.Result{}, err
		}

		err = setupGCPCredentials(ctx, network, cluster, infraClient, tc, tcClient, &sidecar)
		if err != nil {
			return ctrl.Result{}, err
		}

		if metricsPort != 0 {
			if tc.Spec.TiDB.Annotations == nil {
				tc.Spec.TiDB.Annotations = make(map[string]string)
			}
			tc.Spec.TiDB.Annotations[metricsPortAnnotationKey] = strconv.Itoa(metricsPort)
		}

		tc.Spec.TiDB.SlowLogVolumeName = storageVolume.Name
		tc.Spec.TiDB.StorageVolumes = addStorageVolumeIfNotExist(tc.Spec.TiDB.StorageVolumes, storageVolume)
		tc.Spec.TiDB.AdditionalContainers = addContainerIfNotExist(tc.Spec.TiDB.AdditionalContainers, sidecar)
		tc.Spec.TiDB.Plugins = addStringIfNotExist(tc.Spec.TiDB.Plugins, pluginName)
		tc.Spec.TiDB.AdditionalVolumes = addVolumeIfNotExist(tc.Spec.TiDB.AdditionalVolumes, volume)
	} else {
		err := cleanupGCPCredentials(ctx, network, cluster, infraClient, tc, tcClient, &sidecar)
		if err != nil {
			return ctrl.Result{}, err
		}

		delete(tc.Spec.TiDB.Annotations, metricsPortAnnotationKey)

		tc.Spec.TiDB.SlowLogVolumeName = ""
		// Do not support update, operator will just ignore it.
		// we do not force delete STS and delete PVC when disable audit log.
		tc.Spec.TiDB.StorageVolumes = deleteStorageVolume(tc.Spec.TiDB.StorageVolumes, storageVolume.Name)
		tc.Spec.TiDB.AdditionalContainers = deleteContainer(tc.Spec.TiDB.AdditionalContainers, sidecar)
		tc.Spec.TiDB.Plugins = deleteString(tc.Spec.TiDB.Plugins, pluginName)
		tc.Spec.TiDB.AdditionalVolumes = deleteVolume(tc.Spec.TiDB.AdditionalVolumes, volume.Name)
	}

	if !equality.Semantic.DeepEqual(oldTC.Spec, tc.Spec) {
		logger.Info("audit log related fields are changed, will update tc spec")
		err := tcClient.Update(ctx, tc)
		if err != nil {
			return ctrl.Result{}, errors.Annotate(err, "failed to update tc")
		}
	}

	if audit != nil && audit.Enable {
		stsName := tc.Name + "-tidb"
		sts := new(apps.StatefulSet)
		key := types.NamespacedName{Namespace: tc.Namespace, Name: stsName}
		var err error
		if advanceSTS {
			err = getAdvanceStatefulset(ctx, tcClient, key, sts)
		} else {
			err = tcClient.Get(ctx, key, sts)
		}
		if err != nil {
			if apierrors.IsNotFound(err) {
				return ctrl.Result{RequeueAfter: recheckDuration}, nil
			}
			return ctrl.Result{}, errors.Annotate(err, "failed to get tidb sts")
		}

		// operator will add "tidb-" prefix to volume name in tc.Spec.TiDB.StorageVolumes
		volumeName := "tidb-" + slowlogVolumeName
		existVolume := false
		for _, entry := range sts.Spec.VolumeClaimTemplates {
			if entry.Name == volumeName {
				existVolume = true
				break
			}
		}

		if !existVolume {
			logger.Info("delete sts", "sts", sts.Name)
			var err error
			if advanceSTS {
				err = deleteAdvanceStatefuleset(ctx, tcClient, client.ObjectKeyFromObject(sts), client.PropagationPolicy(metav1.DeletePropagationOrphan))
			} else {
				err = tcClient.Delete(ctx, sts, client.PropagationPolicy(metav1.DeletePropagationOrphan))
			}
			if err != nil {
				if apierrors.IsNotFound(err) {
					// Since we get the STS first.
					// This only happen when cache client is not updated and the new STS is not recreate by operator yet.
					logger.Info("cached not updated and sts not recreate yet, keep requeue after sometime")
					return ctrl.Result{RequeueAfter: recheckDuration}, nil
				}
				return ctrl.Result{}, err
			}
			return ctrl.Result{RequeueAfter: recheckDuration}, nil
		}

		for _, ord := range sets.List(GetPodOrdinals(tc.Spec.TiDB.Replicas, sts)) {
			podName := sts.Name + "-" + strconv.Itoa(int(ord))
			po := new(corev1.Pod)
			err := tcClient.Get(ctx, types.NamespacedName{Namespace: tc.Namespace, Name: podName}, po)
			if err != nil {
				if apierrors.IsNotFound(err) {
					return ctrl.Result{RequeueAfter: recheckDuration}, nil
				}
				return ctrl.Result{}, err
			}
			isNewPod := false
			for _, v := range po.Spec.Volumes {
				if v.Name == volumeName {
					isNewPod = true
					break
				}
			}

			if !isNewPod {
				logger.Info("delete pod", "pod", po.Name)
				err := tcClient.Delete(ctx, po)
				if err != nil {
					return ctrl.Result{}, err
				}
				return ctrl.Result{RequeueAfter: recheckDuration}, nil
			}

			if !isPodReady(po) {
				logger.Info("wait pod ready", "pod", po.Name)
				return ctrl.Result{RequeueAfter: recheckDuration}, nil
			}
		}

		// all Pod is check and ready now
		if !getAuditReady(cluster) {
			setAuditReady(cluster, true)
			err = infraClient.Status().Update(ctx, cluster)
			if err != nil {
				return ctrl.Result{}, errors.Annotate(err, "failed to update cluster status")
			}
		}
		return ctrl.Result{}, nil
	}

	// Do not wait the update finished.
	if getAuditReady(cluster) {
		setAuditReady(cluster, false)
		err := infraClient.Status().Update(ctx, cluster)
		if err != nil {
			return ctrl.Result{}, errors.Annotate(err, "failed to update cluster status")
		}
	}
	return ctrl.Result{}, nil
}

const gcpCredentialPath = "/.gcp/credentials.json"
const credentialsVolumeName = "audit-credentials"

func secretName(tc *v1alpha1.TidbCluster) string {
	return tc.Name + "-audit"
}

func setupGCPCredentials(
	ctx context.Context,
	network *cloudv1.Network,
	cluster *cloudv1.Cluster,
	infraClient client.Client,
	tc *v1alpha1.TidbCluster,
	tcClient client.Client,
	sidecar *corev1.Container,
) error {
	getter, ok := gcpCredentials[network.Spec.Provider]
	if !ok {
		return nil
	}

	data, err := getter(ctx, infraClient, network)
	if err != nil {
		return err
	}

	sidecar.Env = append(sidecar.Env, corev1.EnvVar{
		Name:  "GOOGLE_APPLICATION_CREDENTIALS",
		Value: gcpCredentialPath,
	})

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: tc.Namespace,
			Name:      secretName(tc),
		},
	}

	_, err = controllerutil.CreateOrUpdate(ctx, tcClient, secret, func() error {
		secret.Data = map[string][]byte{
			filepath.Base(gcpCredentialPath): data,
		}
		return nil
	})
	if err != nil {
		return errors.Annotate(err, "failed to create or update secret")
	}

	v := corev1.Volume{
		Name: credentialsVolumeName,
		VolumeSource: corev1.VolumeSource{
			// ref https://github.com/kubernetes/kubernetes/issues/84880
			// DO NOT specific Items since bug of k8s 1.16 and we still using 1.16 for gcp.
			Secret: &corev1.SecretVolumeSource{
				SecretName: secretName(tc),
			},
		},
	}

	vm := corev1.VolumeMount{
		Name:      credentialsVolumeName,
		MountPath: filepath.Dir(gcpCredentialPath),
	}

	tc.Spec.TiDB.AdditionalVolumes = addVolumeIfNotExist(tc.Spec.TiDB.AdditionalVolumes, v)
	sidecar.VolumeMounts = addVolumeMountIfNotExist(sidecar.VolumeMounts, vm)

	return nil
}

func cleanupGCPCredentials(
	ctx context.Context,
	network *cloudv1.Network,
	cluster *cloudv1.Cluster,
	infraClient client.Client,
	tc *v1alpha1.TidbCluster,
	tcClient client.Client,
	sidecar *corev1.Container,
) error {
	_, ok := gcpCredentials[network.Spec.Provider]
	if !ok {
		return nil
	}

	secret := &corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Namespace: tc.Namespace,
			Name:      secretName(tc),
		},
	}
	err := tcClient.Delete(ctx, secret)
	if err != nil && !apierrors.IsNotFound(err) {
		return errors.Annotate(err, "failed to delete secret")
	}

	deleteVolume(tc.Spec.TiDB.AdditionalVolumes, credentialsVolumeName)

	return nil
}

func getAuditReady(cluster *cloudv1.Cluster) bool {
	if cluster.Status.DB == nil {
		return false
	}

	if cluster.Status.DB.TiDB == nil {
		return false
	}

	return cluster.Status.DB.TiDB.AuditReady
}

func setAuditReady(cluster *cloudv1.Cluster, ready bool) {
	if cluster.Status.DB == nil {
		cluster.Status.DB = &cloudv1.DBStatus{
			// The field is enum, can not be an empty string.
			SecondaryStatus: cloudv1.DBSecondaryStatusUnhealthy,
		}
	}

	if cluster.Status.DB.TiDB == nil {
		cluster.Status.DB.TiDB = new(cloudv1.TiDBStatus)
	}

	cluster.Status.DB.TiDB.AuditReady = ready
}

func isPodReady(po *corev1.Pod) bool {
	for _, c := range po.Status.Conditions {
		if c.Type == corev1.PodReady {
			if c.Status == corev1.ConditionTrue {
				return true
			}
			return false
		}
	}

	return false
}

func addStringIfNotExist(strs []string, str string) []string {
	for _, entry := range strs {
		if entry == str {
			return strs
		}
	}

	return append(strs, str)
}

func deleteString(strs []string, str string) []string {
	for idx, entry := range strs {
		if entry == str {
			return append(strs[:idx], strs[idx+1:]...)
		}
	}

	return strs
}

func containString(strs []string, str string) bool {
	for _, entry := range strs {
		if entry == str {
			return true
		}
	}

	return false
}

func addContainerIfNotExist(containers []corev1.Container, c corev1.Container) []corev1.Container {
	for _, entry := range containers {
		if entry.Name == c.Name {
			return containers
		}
	}

	return append(containers, c)
}

func deleteContainer(containers []corev1.Container, c corev1.Container) []corev1.Container {
	for idx, entry := range containers {
		if entry.Name == c.Name {
			return append(containers[:idx], containers[idx+1:]...)
		}
	}

	return containers
}

func containContainer(containers []corev1.Container, name string) bool {
	for _, entry := range containers {
		if entry.Name == name {
			return true
		}
	}

	return false
}

func addStorageVolumeIfNotExist(volumes []v1alpha1.StorageVolume, volume v1alpha1.StorageVolume) []v1alpha1.StorageVolume {
	for _, entry := range volumes {
		if entry.Name == volume.Name {
			return volumes
		}
	}

	return append(volumes, volume)
}

func deleteStorageVolume(volumes []v1alpha1.StorageVolume, name string) []v1alpha1.StorageVolume {
	for idx, entry := range volumes {
		if entry.Name == name {
			return append(volumes[:idx], volumes[idx+1:]...)
		}
	}

	return volumes
}

func containStorageVolume(volumes []v1alpha1.StorageVolume, name string) bool {
	for _, entry := range volumes {
		if entry.Name == name {
			return true
		}
	}

	return false
}

func addVolumeIfNotExist(volumes []corev1.Volume, volume corev1.Volume) []corev1.Volume {
	for _, entry := range volumes {
		if entry.Name == volume.Name {
			return volumes
		}
	}

	return append(volumes, volume)
}

func deleteVolume(volumes []corev1.Volume, name string) []corev1.Volume {
	for idx, entry := range volumes {
		if entry.Name == name {
			return append(volumes[:idx], volumes[idx+1:]...)
		}
	}

	return volumes
}

func addVolumeMountIfNotExist(vms []corev1.VolumeMount, vm corev1.VolumeMount) []corev1.VolumeMount {
	for _, entry := range vms {
		if entry.Name == vm.Name {
			return vms
		}
	}

	return append(vms, vm)
}

func getExternalID(network *cloudv1.Network, cluster *cloudv1.Cluster) string {
	if network.Status.IAM.ExternalId != "" {
		return network.Status.IAM.ExternalId
	}
	return cluster.GetLabels()[consts.ProjectTagKey]
}
