package eks

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

	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"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"
	"github.com/tidbcloud/infra-provider/pkg/nodegroup"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

type NodePool struct {
	*task.BaseTask
}

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

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

func (t *NodePool) Create(ctx context.Context) deploy.Result {
	// import IAM role must be ready (AWSNodeRoleARN present)
	if res := ensureImportNodeRoleARNReady(t.Import.Status.AWSNodeRoleARN); !res.IsDoneState() {
		return res
	}

	// create launch template
	lt := &ec2v1alpha1.LaunchTemplate{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "ec2.aws.crossplane.io/v1alpha1",
			Kind:       "LaunchTemplate",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getLaunchTemplateName(),
		},
	}

	labels := common.GetNodeLabels(t.Namer, t.Import)
	ltMutateFunc := func() error {
		lt.Labels = labels
		// assume default disk size is 20GiB
		size := int64(20)
		region := t.GetRegion()

		lt.Spec = ec2v1alpha1.LaunchTemplateSpec{
			ForProvider: ec2v1alpha1.LaunchTemplateParameters{
				Region:             region,
				LaunchTemplateName: ptr.To(t.getLaunchTemplateName()),
				LaunchTemplateData: &ec2v1alpha1.RequestLaunchTemplateData{
					BlockDeviceMappings: nodegroup.GetBlockDeviceMappingsForBottlerocket(size),
					TagSpecifications: []*ec2v1alpha1.LaunchTemplateTagSpecificationRequest{
						{
							ResourceType: ptr.To("instance"),
							Tags: []*ec2v1alpha1.Tag{
								{
									Key:   ptr.To("Name"),
									Value: ptr.To(fmt.Sprintf("import-%s", t.Context.Import.Name)),
								},
							},
						},
					},
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.GetEKSProviderConfigName(),
				},
			},
		}
		return nil
	}

	if _, err := controllerutil.CreateOrUpdate(ctx, t.Context.Local, lt, ltMutateFunc); err != nil {
		t.Logger.Error(err, "fail to create launch template "+t.getLaunchTemplateName())
		return deploy.Error(err)
	}

	// create node group
	nodePool, err := t.desiredNodePools()
	if err != nil {
		t.Logger.Error(err, "fail to get node group "+t.generateNodePoolName())
		return deploy.Error(err)
	}

	if err := k8s.Apply(ctx, t.Context.Local, nodePool); err != nil {
		t.Logger.Error(err, "fail to apply node group "+t.generateNodePoolName())
		return deploy.Error(err)
	}

	return deploy.Done()
}

func (t *NodePool) PostCreate(ctx context.Context) deploy.Result {
	if res := ensureImportNodeRoleARNReady(t.Import.Status.AWSNodeRoleARN); !res.IsDoneState() {
		return res
	}

	nodePoolName := t.generateNodePoolName()
	ng := manualv1alpha1.NodeGroup{
		ObjectMeta: metav1.ObjectMeta{
			Name: nodePoolName,
		},
	}
	if err := t.Context.Local.Get(ctx, client.ObjectKeyFromObject(&ng), &ng); err != nil {
		if apierrors.IsNotFound(err) {
			return deploy.Wait(deploy.DefaultWaitRequeueAfter, "node group %s has not been created", nodePoolName)
		}
		t.Context.Logger.Error(err, "fail to get node group "+nodePoolName)
		return deploy.Error(err)
	}

	// CREATE_FAILED/ERROR require manual recovery (reboot instance or recreate the nodegroup).
	// Do not mark the import as failed — once the nodegroup is recovered and becomes ACTIVE
	// the controller will proceed automatically on the next reconcile.
	if ng.Status.AtProvider.Status == "CREATE_FAILED" || ng.Status.AtProvider.Status == "ERROR" {
		t.Context.Logger.Error(fmt.Errorf("nodegroup requires manual recovery"),
			"nodegroup is in a failed state; please reboot instance or recreate the nodegroup to unblock the import",
			"nodegroup", nodePoolName,
			"status", ng.Status.AtProvider.Status,
			"health", ng.Status.AtProvider.Health)
		return deploy.Wait(deploy.DefaultWaitRequeueAfter, "nodegroup %s requires manual recovery (status: %s)", nodePoolName, ng.Status.AtProvider.Status)
	}

	if ng.Status.AtProvider.Status == "ACTIVE" {
		return deploy.Done()
	}
	return deploy.Wait(deploy.DefaultWaitRequeueAfter, "node group not ready: %s/%s", nodePoolName, ng.Status.AtProvider.Status)
}

func (t *NodePool) Delete(ctx context.Context) deploy.Result {
	// delete node group
	nodePool := &manualv1alpha1.NodeGroup{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1alpha1",
			Kind:       "NodeGroup",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: t.generateNodePoolName(),
		},
	}
	if err := k8s.DeleteObject(ctx, t.Context.Local, nodePool); err != nil {
		return deploy.Error(err)
	}

	// delete launch template
	lt := &ec2v1alpha1.LaunchTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getLaunchTemplateName(),
		},
	}
	return deploy.ErrorOrDone(k8s.DeleteObject(ctx, t.Context.Local, lt))
}

func (t *NodePool) PostDelete(ctx context.Context) deploy.Result {
	nodePool := &manualv1alpha1.NodeGroup{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1alpha1",
			Kind:       "NodeGroup",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name: t.generateNodePoolName(),
		},
	}
	if err := k8s.DeleteObjectCompleted(ctx, t.Context.Local, nodePool); err != nil {
		return deploy.Error(err).WithRequeuAfter(time.Second * 20)
	}

	lt := &ec2v1alpha1.LaunchTemplate{
		ObjectMeta: metav1.ObjectMeta{
			Name: t.getLaunchTemplateName(),
		},
	}
	return deploy.ErrorOrDone(k8s.DeleteObjectCompleted(ctx, t.Context.Local, lt))
}

func (t *NodePool) generateNodePoolName() string {
	// example: "nightly-123-123", "c5.9xlarge", "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("import-ng-%s", hashString[:11])
	return result
}

func (t *NodePool) getLaunchTemplateName() string {
	return fmt.Sprintf("import-lt-%s", t.Context.Import.Name)
}

func (t *NodePool) getAWSNodeRoleName() string {
	// assumeRoleArn := util.GetRoleARN(t.EnvProvider.GetAccount(), t.Config.FullManage.AssumeRoleName)
	// return assumeRoleArn

	return t.Import.Status.AWSNodeRoleARN
}

// ensureImportNodeRoleARNReady validates the provided AWS Node Role ARN is present.
// If empty, it returns a retriable deploy.Result that requeues after iamReadyRequeueInterval.
// If non-empty, returns Done.
func ensureImportNodeRoleARNReady(arn string) deploy.Result {
	if arn == "" {
		return deploy.Wait(iamReadyRequeueInterval, "AWS node role ARN is not available in import status")
	}
	return deploy.Done()
}

func (t *NodePool) desiredNodePools() (*manualv1alpha1.NodeGroup, error) {
	labels := common.GetNodeLabels(t.Namer, t.Import)
	region := t.GetRegion()

	nodeRole := t.getAWSNodeRoleName()
	instanceType := t.Config.ImportConfig.InstanceType

	nodePool := &manualv1alpha1.NodeGroup{
		TypeMeta: metav1.TypeMeta{
			APIVersion: "eks.aws.crossplane.io/v1alpha1",
			Kind:       "NodeGroup",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:   t.generateNodePoolName(),
			Labels: labels,
		},
		Spec: manualv1alpha1.NodeGroupSpec{
			ForProvider: manualv1alpha1.NodeGroupParameters{
				Region:      region,
				AMIType:     &t.Config.Network.AdminNodesAMIType,
				ClusterName: t.Context.Kubernetes.Name,
				NodeRole:    nodeRole,
				ScalingConfig: &manualv1alpha1.NodeGroupScalingConfig{
					DesiredSize: ptr.To[int32](1),
					MaxSize:     ptr.To[int32](1),
					MinSize:     ptr.To[int32](1),
				},
				Subnets:       t.Subnets(),
				InstanceTypes: []string{instanceType},
				Labels:        labels,
				Taints: []manualv1alpha1.Taint{
					{
						Key:    ptr.To("dedicated"),
						Value:  ptr.To(fmt.Sprintf("tidb-%s", task.LightningComponent)),
						Effect: util.ToCrossplaneTaintEffect(corev1.TaintEffectNoSchedule),
					},
				},
				CapacityType: ptr.To("ON_DEMAND"),
				LaunchTemplate: &manualv1alpha1.LaunchTemplateSpecification{
					Name: ptr.To(t.getLaunchTemplateName()),
				},
				UpdateConfig: &manualv1alpha1.NodeGroupUpdateConfig{
					MaxUnavailable: ptr.To[int32](1),
				},
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{
					Name: t.GetEKSProviderConfigName(),
				},
			},
		},
	}

	return nodePool, nil
}
