package provider

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"fmt"

	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/upbound/provider-azure/apis/containerservice/v1beta1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

const (
	maxNodeCountPerZone   = 50
	defaultRootDiskSizeGB = 50
)

type NodePoolProvider interface {
	GetProviderName() string
	// CreateNodePool creates or update a nodepool, and returns the cloud provider nodepool name
	CreateNodePool(context.Context, *NodePool) (string, error)
	// DeleteNodePool deletes a nodepool by nodepool name
	DeleteNodePool(context.Context, string) error
}

// NodePool is a general nodepool abstraction. On GCP and Azure, it represents
// node pool and on AWS, it may represent asg or nodegroup.
// Different cloud providers may have different naming convention, hence the real resource name and tags may be different from the Name and Tags fields
// It's handled in the specific cloud provider implementation
type NodePool struct {
	Name           string
	Arch           string
	Region         string
	Zones          []string
	NodeCount      int32
	InstanceType   string
	RootDiskSizeGB int64
	KubeClusterID  string
	NodeSubnets    []string
	PodSubnets     []string
	Taints         []corev1.Taint
	// Labels are k8s node labels
	Labels map[string]string
	// key-value pairs attached to cloud resources
	Tags map[string]string
}

type nameGenerator = func(string) string

type azureNodePool struct {
	cli            client.Client
	providerConfig string
	providerName   string
	// NOTE: Please take care of existing resources when modifying the namer function
	namer nameGenerator
}

func NewAzureNodePool(cli client.Client, providerConfig string) NodePoolProvider {
	return &azureNodePool{
		cli:            cli,
		providerConfig: providerConfig,
		providerName:   "Azure",
		namer:          genAksNodePoolName,
	}
}

func (n *azureNodePool) GetProviderName() string {
	return n.providerName
}

func (n *azureNodePool) CreateNodePool(ctx context.Context, nodepool *NodePool) (string, error) {
	taints := []*string{}
	for _, t := range nodepool.Taints {
		taint := fmt.Sprintf("%s=%s:%s", t.Key, t.Value, t.Effect)
		taints = append(taints, ptr.To(taint))
	}
	if len(nodepool.NodeSubnets) < 1 || len(nodepool.PodSubnets) < 1 {
		return "", fmt.Errorf("missing node or pod subnet")
	}
	diskSize := ptr.To(float64(defaultRootDiskSizeGB))
	if nodepool.RootDiskSizeGB != 0 {
		diskSize = ptr.To(float64(nodepool.RootDiskSizeGB))
	}
	np := &v1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: n.namer(nodepool.Name),
		},
		Spec: v1beta1.KubernetesClusterNodePoolSpec{
			ForProvider: v1beta1.KubernetesClusterNodePoolParameters{
				VMSize:              ptr.To(nodepool.InstanceType),
				OsSku:               ptr.To("AzureLinux"),
				OsDiskSizeGb:        diskSize,
				Zones:               util.ToSlicePointer(nodepool.Zones),
				Tags:                util.ToMapPointer(nodepool.Tags),
				NodeLabels:          util.ToMapPointer(nodepool.Labels),
				NodeTaints:          taints,
				KubernetesClusterID: ptr.To(nodepool.KubeClusterID),
				VnetSubnetID:        ptr.To(nodepool.NodeSubnets[0]),
				PodSubnetID:         ptr.To(nodepool.PodSubnets[0]),
				// if it is not set, the default value is 10%, and aks will be frequently update by crossplane
				// because it is not set by crossplane lateinitialize
				// https://github.com/crossplane-contrib/provider-upjet-azure/issues/686
				UpgradeSettings: []v1beta1.KubernetesClusterNodePoolUpgradeSettingsParameters{
					{
						MaxSurge: ptr.To("10%"),
					},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: n.providerConfig,
				},
			},
		},
	}
	if nodepool.NodeCount != 0 {
		np.Spec.ForProvider.NodeCount = ptr.To(float64(nodepool.NodeCount))
	} else {
		np.Spec.ForProvider.EnableAutoScaling = ptr.To(true)
		np.Spec.ForProvider.MinCount = ptr.To(float64(0))
		np.Spec.ForProvider.MaxCount = ptr.To(float64(maxNodeCountPerZone))
	}
	if err := k8s.Apply(ctx, n.cli, np); err != nil {
		return "", err
	}
	key := types.NamespacedName{Name: n.namer(nodepool.Name)}
	if err := n.cli.Get(ctx, key, np); err != nil {
		return "", err
	}
	for _, cond := range np.Status.ResourceStatus.ConditionedStatus.Conditions {
		if cond.Reason != xpv1.ReasonAvailable {
			continue
		}
		poolID := np.Status.AtProvider.ID
		if poolID == nil {
			break
		}

		if cond.Status == corev1.ConditionTrue && cond.Type == xpv1.TypeReady {
			return *poolID, nil
		}
	}
	return "", fmt.Errorf("nodepool not ready")
}

func (n *azureNodePool) DeleteNodePool(ctx context.Context, name string) error {
	nodepool := &v1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: n.namer(name),
		},
	}
	return k8s.DeleteObjectCompleted(ctx, n.cli, nodepool)
}

// https://learn.microsoft.com/en-us/troubleshoot/azure/azure-kubernetes/create-upgrade-delete/aks-common-issues-faq#what-naming-restrictions-are-enforced-for-aks-resources-and-parameters-
// AKS node pool names must be all lowercase. The names must be 1-12 characters in length for Linux node pools and 1-6 characters for Windows node pools. A name must start with a letter, and the only allowed characters are letters and numbers.
func genAksNodePoolName(name string) string {
	hash := md5.Sum([]byte(name))
	hashString := hex.EncodeToString(hash[:])
	// prefix with `n` to avoid name starts with a number
	return fmt.Sprintf("n%s", hashString[:11])
}
