package v1

import (
	"context"
	"sort"
	"strconv"
	"strings"

	"github.com/pingcap/errors"
	corev1 "k8s.io/api/core/v1"
	storagev1 "k8s.io/api/storage/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
)

// FindRegion return the specific name region.
func (c *Provider) FindRegion(name string) (region Region, err error) {
	for _, r := range c.Spec.Regions {
		if r.Name == name {
			return r, nil
		}
	}

	return Region{}, errors.Errorf("no region %q", name)
}

func (c *Provider) GetInstanceTypes() ([]InstanceType, error) {
	return c.Spec.InstanceTypes, nil
}

func (c *Provider) GetProviderType() string {
	return c.Spec.Type
}

func (c *Provider) FindRegionZones(regionName string) ([]string, error) {
	region, err := c.FindRegion(regionName)
	if err != nil {
		return nil, err
	}
	var zones []string
	zones = append(zones, region.Zones...)
	// make sure the zones are sorted, else might trigger some cluster resource update, such as node pool.
	sort.Strings(zones)
	return zones, nil
}

// FindInstanceType find the instanceType by name, an error is raised if not found
func (c *Provider) FindInstanceType(name string) (InstanceType, error) {
	for _, t := range c.Spec.InstanceTypes {
		if t.Name == name {
			return t, nil
		}
	}
	return InstanceType{}, errors.Errorf("instance type %q not found", name)
}

// FindStorageClass find the StorageClass by name, an error is raised if not found
func (c *Provider) FindStorageClass(name string) (StorageClass, error) {
	for _, sc := range c.Spec.StorageClasses {
		if name == sc.Name {
			return sc, nil
		}
	}

	return StorageClass{}, errors.Errorf("storage class %q not found", name)
}

type CloudProvider string

const (
	CloudProviderAWS   = "aws"
	CloudProviderGCP   = "gcp"
	CloudProviderAzure = "azure"
)

// We assume the name is {component}-{storageClassName}
// where {storageClassName} has the type prefix, etc: gp2 | gp3-4000-400 | pd-ssd
func TypeOfStorageFromName(name string) (cp CloudProvider, tp string, err error) {
	ar := strings.SplitN(name, "-", 2)
	if len(ar) < 2 {
		return "", "", errors.Errorf("unknown name %q, should be {component}-{storageClassName}", name)
	}

	scn := ar[1]
	tps := []string{"gp2", "gp3", "io1", "io2"}

	for _, v := range tps {
		if strings.HasPrefix(scn, v) {
			return CloudProviderAWS, v, nil
		}
	}

	// gcp ref: https://cloud.google.com/compute/docs/disks#pdspecs
	tps = []string{"pd-standard", "pd-balanced", "pd-ssd", "pd-extreme"}
	for _, v := range tps {
		if strings.HasPrefix(scn, v) {
			return CloudProviderGCP, v, nil
		}
	}

	return "", "", errors.Errorf("unknown type of block storage")
}

func EnsureClusterStorageClass(ctx context.Context, client client.Client, p *Provider, cluster *Cluster) error {
	return ensureClusterStorageClass(ctx, client, p, cluster, false)
}

func EnsureClusterStorageClassAWSInTreeProvisioner(ctx context.Context, client client.Client, p *Provider, cluster *Cluster) error {
	return ensureClusterStorageClass(ctx, client, p, cluster, true)
}

func ensureClusterStorageClass(ctx context.Context, client client.Client, p *Provider, cluster *Cluster, awsInTreeProvisioner bool) error {
	name := cluster.Spec.Cluster.PD.StorageClassName
	if name != "" {
		sc, err := p.FindStorageClass(name)
		if err != nil {
			return err
		}
		err = ensureProviderStorageClass(ctx, client, sc, awsInTreeProvisioner)
		if err != nil {
			return err
		}
	}

	name = cluster.Spec.Cluster.TiKV.StorageClassName
	if name != "" {
		sc, err := p.FindStorageClass(name)
		if err != nil {
			return err
		}
		err = ensureProviderStorageClass(ctx, client, sc, awsInTreeProvisioner)
		if err != nil {
			return err
		}
	}

	name = cluster.Spec.Cluster.TiFlash.StorageClassName
	if name != "" {
		sc, err := p.FindStorageClass(name)
		if err != nil {
			return err
		}
		err = ensureProviderStorageClass(ctx, client, sc, awsInTreeProvisioner)
		if err != nil {
			return err
		}
	}

	return nil
}

// EnsureProviderStorageClass create the specific storage class.
func EnsureProviderStorageClass(ctx context.Context, client client.Client, sc StorageClass) error {
	return ensureProviderStorageClass(ctx, client, sc, false)
}

// EnsureProviderStorageClassAWSIntreeProvisioner work like EnsureProviderStorageClass
// but for aws, it use kubernetes.io/aws-ebs provisioner, which means it DO NOT support gp3.
// Only k8s still not install aws-ebs-csi-driver, and using in-tree provisioner should use this.
func EnsureProviderStorageClassAWSIntreeProvisioner(ctx context.Context, client client.Client, sc StorageClass) error {
	return ensureProviderStorageClass(ctx, client, sc, true)
}

func ensureProviderStorageClass(ctx context.Context, client client.Client, sc StorageClass, awsInTreeProvisioner bool) error {
	cloudProvider, tp, err := TypeOfStorageFromName(sc.Name)
	if err != nil {
		return err
	}

	param := map[string]string{
		"type": tp,
	}

	if sc.IOPS != 0 {
		param["iops"] = strconv.Itoa(sc.IOPS)
	}

	if sc.Throughput != 0 {
		param["throughput"] = strconv.Itoa(sc.Throughput)
	}

	switch cloudProvider {
	case CloudProviderAWS:
		if awsInTreeProvisioner {
			err = ensureAWSStorageClassInTree(ctx, client, sc.Name, param)
			if err != nil {
				return err
			}
		} else {
			err = EnsureAWSStorageClass(ctx, client, sc.Name, param)
			if err != nil {
				return err
			}
		}
	case CloudProviderGCP:
		err = EnsureGCPStorageClass(ctx, client, sc.Name, param)
		if err != nil {
			return err
		}
	default:
		return errors.Errorf("unknown cloud provider %q", cloudProvider)
	}

	return nil
}

// ensureAWSStorageClassInTree ensure create the storage class using the in-tree provisioner.
func ensureAWSStorageClassInTree(ctx context.Context, cli client.Client, name string, params map[string]string) error {
	waitForFirstConsumer := storagev1.VolumeBindingWaitForFirstConsumer
	reclaimDelete := corev1.PersistentVolumeReclaimDelete

	storageClass := &storagev1.StorageClass{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		VolumeBindingMode:    &waitForFirstConsumer,
		AllowVolumeExpansion: pointer.Bool(true),
		ReclaimPolicy:        &reclaimDelete,
		Provisioner:          "kubernetes.io/aws-ebs",
		Parameters: map[string]string{
			"fstype":    "ext4",
			"encrypted": "true",
		},
		MountOptions: []string{"nodelalloc,noatime"},
	}
	for k, v := range params {
		storageClass.Parameters[k] = v
	}

	_, err := controllerutil.CreateOrUpdate(ctx, cli, storageClass, func() error {
		return nil
	})
	if err != nil {
		return errors.Annotate(err, "fail to create or update storage class")
	}

	return nil
}

// EnsureAWSStorageClass create the storage class if not exist.
// ref: https://github.com/kubernetes-sigs/aws-ebs-csi-driver#createvolume-parameters
func EnsureAWSStorageClass(ctx context.Context, cli client.Client, name string, params map[string]string) error {
	waitForFirstConsumer := storagev1.VolumeBindingWaitForFirstConsumer
	reclaimDelete := corev1.PersistentVolumeReclaimDelete

	storageClass := &storagev1.StorageClass{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		VolumeBindingMode:    &waitForFirstConsumer,
		AllowVolumeExpansion: pointer.Bool(true),
		ReclaimPolicy:        &reclaimDelete,
		Provisioner:          "ebs.csi.aws.com",
		Parameters: map[string]string{
			"csi.storage.k8s.io/fstype": "ext4",
			"encrypted":                 "true",
		},
		MountOptions: []string{"nodelalloc,noatime"},
	}
	for k, v := range params {
		storageClass.Parameters[k] = v
	}

	_, err := controllerutil.CreateOrUpdate(ctx, cli, storageClass, func() error {
		return nil
	})
	if err != nil {
		return errors.Annotate(err, "fail to create or update storage class")
	}

	return nil
}

func EnsureGCPStorageClass(ctx context.Context, cli client.Client, name string, params map[string]string) error {
	waitForFirstConsumer := storagev1.VolumeBindingWaitForFirstConsumer
	reclaimDelete := corev1.PersistentVolumeReclaimDelete

	storageClass := &storagev1.StorageClass{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
		VolumeBindingMode:    &waitForFirstConsumer,
		AllowVolumeExpansion: pointer.Bool(true),
		ReclaimPolicy:        &reclaimDelete,
		Provisioner:          "kubernetes.io/gce-pd",
		Parameters:           map[string]string{},
		MountOptions:         []string{"nodelalloc,noatime"},
	}
	for k, v := range params {
		storageClass.Parameters[k] = v
	}

	_, err := controllerutil.CreateOrUpdate(ctx, cli, storageClass, func() error {
		return nil
	})
	if err != nil {
		return errors.Annotate(err, "fail to create or update storage class")
	}

	return nil
}
