package aks

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"errors"
	"fmt"
	"sort"
	"strings"

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

	"github.com/tidbcloud/eks-provider/controllers/imports/task"
	"github.com/tidbcloud/eks-provider/controllers/imports/task/common"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

type NodePool struct {
	*task.BaseTask
}

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

func NewNodePool(ctx *task.Context) *NodePool {
	return &NodePool{
		BaseTask: &task.BaseTask{Context: ctx},
	}
}

func (t *NodePool) Create(ctx context.Context) deploy.Result {
	nodePool, err := t.desiredNodePools()
	if err != nil {
		return deploy.Error(err)
	}
	if _, err = controllerutil.CreateOrUpdate(ctx, t.Local, nodePool, func() error { return nil }); err != nil {
		return deploy.Error(err)
	}

	return deploy.Done()
}

func (t *NodePool) Delete(ctx context.Context) deploy.Result {
	nodePool := &v1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.GetAKSNodePoolName(),
		},
	}
	return deploy.ErrorOrDone(k8s.DeleteObject(ctx, t.Local, nodePool))
}

func (t *NodePool) PostDelete(ctx context.Context) deploy.Result {
	return deploy.Done()
}

// desiredNodePools generate nodepools expected to createOrUpdate
func (t *NodePool) desiredNodePools() (*v1beta1.KubernetesClusterNodePool, error) {

	zoneID, err := t.getZoneID()
	if err != nil {
		return nil, err
	}

	labels := common.GetNodeLabels(t.Namer, t.Import)
	it := t.Config.ImportConfig.InstanceType
	// it := "Standard_D2s_v5"
	nodePool := &v1beta1.KubernetesClusterNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:        t.GetAKSNodePoolName(),
			Labels:      labels,
			Annotations: map[string]string{},
		},
		Spec: v1beta1.KubernetesClusterNodePoolSpec{
			ForProvider: v1beta1.KubernetesClusterNodePoolParameters{
				VMSize:       &it,
				OsDiskSizeGb: ptr.To(float64(100)),
				OsSku:        ptr.To("AzureLinux"),
				Zones:        convertStringSliceToPointerSlice([]string{zoneID}), // https://learn.microsoft.com/en-us/azure/aks/cluster-autoscaler?tabs=azure-cli
				NodeLabels:   convertMapstringToPointerMapString(labels),
				NodeTaints: []*string{
					ptr.To(fmt.Sprintf("dedicated=tidb-%s:NoSchedule", task.LightningComponent)),
				},
				EnableAutoScaling: ptr.To(false),
				NodeCount:         ptr.To(float64(1)),
				KubernetesClusterID: ptr.To(fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/managedClusters/%s",
					t.Kubernetes.Account,
					t.Kubernetes.Cloud.Azure.ResourceGroup,
					t.getAKSClusterRef(),
				)),
				// https://learn.microsoft.com/en-us/azure/aks/configure-azure-cni-dynamic-ip-allocation
				VnetSubnetID: ptr.To(t.Kubernetes.Cloud.Azure.NodeSubnets[0].Name),
				PodSubnetID:  ptr.To(t.Kubernetes.Cloud.Azure.PodSubnets[0].Name),
				// 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: t.GetAKSProviderConfigName(),
				},
				ManagementPolicies: []xpv1.ManagementAction{
					xpv1.ManagementActionCreate,
					xpv1.ManagementActionObserve,
					xpv1.ManagementActionUpdate,
					xpv1.ManagementActionDelete,
				},
			},
		},
	}
	return nodePool, nil
}

func (t *NodePool) getZoneID() (string, error) {
	zones := t.Zones()
	if len(zones) < 1 {
		return "", errors.New("zones is required")
	}

	zoneIDs := make([]string, 0, len(zones))
	for _, zone := range zones {
		if pos := strings.Index(zone, "-"); pos != -1 {
			zone = zone[pos+1:]
		}
		if t.Config.ImportConfig.Azure.Zone != "" &&
			t.Config.ImportConfig.Azure.Zone == zone {
			return zone, nil
		}
		zoneIDs = append(zoneIDs, zone)
	}
	sort.Strings(zoneIDs)
	return zoneIDs[0], nil
}

func (t *NodePool) getAKSClusterRef() string {
	return t.EnvProvider.GetClusterName()
}

// convertStringSliceToPointerSlice converts a slice of strings to a slice of string pointers.
func convertStringSliceToPointerSlice(input []string) []*string {
	output := make([]*string, len(input))
	for i, v := range input {
		output[i] = ptr.To(v)
	}
	return output
}

// convertMapstringToPointerMapString converts a map[string]string to a map[string]*string
func convertMapstringToPointerMapString(input map[string]string) map[string]*string {
	result := make(map[string]*string)
	for key, value := range input {
		result[key] = ptr.To(value)
	}
	return result
}

func (t *NodePool) GetNodeVersion() string {
	return t.Kubernetes.Version
}

func (t *NodePool) GetAKSNodePoolName() string {
	// 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.

	// example: "nightly-123-123", "Standard_DS2_v2", "1.28"
	originalString := fmt.Sprintf("%s/%s/%s/%s",
		t.GetClusterScopeName(),
		t.Import.Name,
		task.LightningComponent,
		t.Config.ImportConfig.InstanceType)

	hash := md5.Sum([]byte(originalString))
	hashString := hex.EncodeToString(hash[:])
	result := fmt.Sprintf("n%s", hashString[:11])
	return result
}
