package common

import (
	"context"
	"fmt"
	"sort"
	"strings"

	storagev1 "k8s.io/api/storage/v1"
	"k8s.io/apimachinery/pkg/util/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/common/audit"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/common/builder"
	"github.com/tidbcloud/eks-provider/controllers/cluster/task/utils"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

var (
	staticSCsUsedByTidbCluster = map[string]bool{
		audit.StorageClassName:                                      true,
		builder.AddIndexAccelerateStorageClassName:                  true,
		audit.HyperdiskBalancedStorageClassName:                     true,
		builder.AddIndexAccelerateHyperdiskBalancedStorageClassName: true,
	}
)

type StorageClass struct {
	*task.BaseTask
}

// NewStorageClass creates customized storage classes with parameters specified from infra-api.
func NewStorageClass(ctx *task.Context) *StorageClass {
	return &StorageClass{BaseTask: &task.BaseTask{Context: ctx}}
}

func (t *StorageClass) Name() string {
	return "StorageClass"
}

func (t *StorageClass) Create(ctx context.Context) deploy.Result {
	cli := t.EnvProvider.GetClient()
	staticSCs, err := t.getStaticStorageClass()
	if err != nil {
		return deploy.Panic(err)
	}
	clusterSCs, err := t.getClusterStorageClass()
	if err != nil {
		return deploy.Panic(err)
	}

	scs := append(staticSCs, clusterSCs...)
	cmekSCs := t.convertCMEKSCsFromIfNeed(ctx, scs)
	scs = append(scs, cmekSCs...)

	createVAC := t.Cluster.FeatureEnabled(consts.ClusterFeatureUseVolumeAttributesClass)
	for _, sc := range scs {
		t.Logger.Info("try to create StorageClass in dataplane k8s", "sc", sc.Name)
		// NOTE: we should use CreateIfNotExist instead of CreateOrUpdate, due to we shouldn't update the existing SC.
		if err := k8s.CreateIfNotExist(ctx, cli, sc); err != nil {
			return deploy.Panic(err)
		}

		vac := utils.GetVolumeAttributesClassFromFromStorageClass(sc)
		if createVAC && vac != nil {
			t.Logger.Info("try to create VolumeAttributesClass in dataplane k8s", "vac", sc.Name)
			if err := k8s.CreateIfNotExist(ctx, cli, vac); err != nil {
				return deploy.Panic(err)
			}
		}
	}

	return deploy.Done()
}

// convertCMEKSCsFromIfNeed converts CMEK SCs from raw SCs if needed.
// CMEK SC is different from raw SC, a customized KMS will be used, instead of default KMS('aws/elb'). So we should create the mirror CMEK SCs for them.
func (t *StorageClass) convertCMEKSCsFromIfNeed(ctx context.Context, scs []*storagev1.StorageClass) []*storagev1.StorageClass {
	var classes []*storagev1.StorageClass
	for _, sc := range scs {
		cmekSC, ok := t.CMEKStorageClassMapper.StorageClass(ctx, t.Cluster, sc)
		if ok {
			classes = append(classes, cmekSC)
		}
	}
	return classes
}

// TODO(tennix): Since multiple clusters may share the same storageclass
// we need to make sure no cluster is using the storageclass before deleting it
func (t *StorageClass) Delete(ctx context.Context) deploy.Result {
	logger := log.FromContext(ctx)

	remote := t.EnvProvider.GetClient()
	// As we create StorageClass per tidb cluster if the cluster is running on Azure and CMEK is enabled,
	// so that we should clean them once the cluster is being deleted.
	// Note:
	//   1. Currently, only Azure tidb clusters with CMEK enabled will create StorageClass per tidb cluster.
	if t.Cluster.FeatureEnabled(consts.ClusterFeatureUseCMEK) {
		scs := &storagev1.StorageClassList{}
		labelFilters := client.MatchingLabels{
			cloudv1.ClusterTagKey: t.Cluster.Name,
			cloudv1.LabelUsedFor:  cloudv1.ClusterFeatureUseCMEK,
		}
		if err := remote.List(ctx, scs, labelFilters); err != nil {
			return deploy.Error(fmt.Errorf("failed to list storageclass for cluster: %w", err))
		}
		errorList := []error{}
		for _, sc := range scs.Items {
			logger.Info("try to delete storageclass", "storageclass", sc.Name)
			if err := k8s.DeleteObjectCompleted(ctx, remote, &sc); err != nil {
				errorList = append(errorList, fmt.Errorf("failed to delete storageclass %s: %w", sc.Name, err))
			}
		}

		if len(errorList) > 0 {
			return deploy.Error(errors.NewAggregate(errorList))
		}
		return deploy.Done()
	}

	return deploy.Done()
}

func (t *StorageClass) getStaticStorageClass() ([]*storagev1.StorageClass, error) {
	staticSCs, err := t.Provider.GetStorageClasses()
	if err != nil {
		return nil, err
	}
	for _, each := range staticSCs {
		usedby := cloudv1.UsedByDbaasControlPlaneShoot
		if staticSCsUsedByTidbCluster[each.Name] {
			usedby = cloudv1.UsedByCustomerTiDB
		}
		if err := t.labelStorageClass(each, usedby); err != nil {
			t.Logger.Error(err, "failed to label storage class", "sc", each.Name)
			continue
		}
	}

	return staticSCs, nil
}

// getClusterStorageClass gets storageclass from Cluster CR components
func (t *StorageClass) getClusterStorageClass() ([]*storagev1.StorageClass, error) {
	var scs []*storagev1.StorageClass
	for _, name := range t.Cluster.GetStorageClassNames() {
		sc, err := utils.ParseStorageClassFromName(name)
		if err != nil {
			t.Logger.Error(err, "skip sync sc", "sc", name)
			continue
		}

		if err := t.labelStorageClass(sc, cloudv1.UsedByCustomerTiDB); err != nil {
			t.Logger.Error(err, "failed to label storage class", "sc", sc.Name)
			continue
		}

		scs = append(scs, sc)
	}
	return scs, nil
}

func (t *StorageClass) labelStorageClass(sc *storagev1.StorageClass, usedby string) error {
	// https://pingcap.feishu.cn/wiki/TEVgwxkifixKJckjYchcPYjknpg
	tags := map[string]string{
		cloudv1.TagLabelServiceType: cloudv1.ServiceTypeDedicated,
		cloudv1.TagLabelTenant:      t.Cluster.GetTenantID(),
		cloudv1.TagLabelProject:     t.Cluster.GetProjectID(),
		cloudv1.TagLabelUsedBy:      usedby,
	}

	// sort keys to make sure the order is consistent
	keys := make([]string, 0, len(tags))
	for key := range tags {
		keys = append(keys, key)
	}
	sort.Strings(keys)

	switch t.Cluster.GetCloudProvider() {
	case cloudv1.CloudProviderAWS:
		// https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/tagging.md
		cnt := 1
		for _, key := range keys {
			value := tags[key]
			tagKey := fmt.Sprintf("tagSpecification_%d", cnt)
			sc.Parameters[tagKey] = fmt.Sprintf("%s=%s", key, value)
			cnt++
		}
	case cloudv1.CloudProviderGCP:
		labels := []string{}
		for _, key := range keys {
			value := tags[key]
			labels = append(labels, fmt.Sprintf("%s=%s", key, value))
		}
		// https://github.com/kubernetes-sigs/gcp-compute-persistent-disk-csi-driver?tab=readme-ov-file
		sc.Parameters["labels"] = strings.Join(labels, ",")
	case cloudv1.CloudProviderAzure:
		labels := []string{}
		for _, key := range keys {
			value := tags[key]
			labels = append(labels, fmt.Sprintf("%s=%s", key, value))
		}
		// https://learn.microsoft.com/en-us/azure/aks/azure-csi-disk-storage-provision
		sc.Parameters["tags"] = strings.Join(labels, ",")
	default:
		return fmt.Errorf("unsupported cloud provider: %s", t.Cluster.GetCloudProvider())
	}

	return nil
}
