package ack

import (
	"context"
	"fmt"
	"strconv"
	"strings"
	"time"

	ackv1alpha1 "github.com/crossplane-contrib/provider-upjet-alibabacloud/apis/ack/v1alpha1"
	xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/consts"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/appbox/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/deploy"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/k8s"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/labels"
	labelutil "github.com/tidbcloud/infra-provider/regional-server/pkg/labels"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/nodepool"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

const (
	defaultInstanceType       = "ecs.g7.large" // Terway CNI does not support all instance types
	defaultNodeMinSize        = 2
	defaultNodeMaxSize        = 10
	defaultSystemDiskCategory = "cloud_essd"
	defaultSystemDiskSize     = 50
)

type nodePool struct {
	*task.BaseTask
}

func NewNodePool(ctx *task.Context) deploy.Task {
	return &nodePool{BaseTask: &task.BaseTask{Context: ctx}}
}

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

func (t *nodePool) Create(ctx context.Context) deploy.Result {
	t.BeginAt = ptr.To(time.Now())

	// TODO: use the right tags, since it's related to node labels.
	tags := t.APPBox.Labels
	tags = controllers.ConvertToCloudTags(tags, consts.ProviderTypeACK)
	tags = util.MergeMaps(tags, t.Config.AdditionalCloudTags)
	labels := util.MergeMaps(t.Namer.Labels(), t.APPBox.Labels)
	subnets := t.VPC.Status.NodeSubnets
	vswitches := []string{}
	for _, subnet := range subnets {
		if subnet.SubnetID != "" {
			vswitches = append(vswitches, subnet.SubnetID)
		}
	}

	// Extract admin node configuration using common utility function
	adminConfig := config.GetAdminNodeConfig(
		t.APPBox.Spec.KubernetesConfig.AdminNodeConfig,
		t.Config.APPBoxConfig.AdminNodeConfig,
		defaultInstanceType,
		defaultNodeMinSize,
		defaultNodeMaxSize,
		defaultSystemDiskSize,
	)

	// Build desired set and create node pools, reusing computed names.
	// O11y pools share APPBox owner labels; include their names in desired so
	// cleanup does not delete them. Enabled=false omits the name → cleanup deletes.
	primaryName := t.nodePoolName()
	desired := map[string]struct{}{primaryName: {}}
	for i := range adminConfig.AdditionalInstanceTypes {
		desired[t.additionalNodePoolName(i)] = struct{}{}
	}

	// Create primary admin node pool
	if err := t.createNodePool(ctx, nodePoolCreateParams{
		name:             primaryName,
		instanceType:     adminConfig.InstanceType,
		minSize:          adminConfig.MinSize,
		maxSize:          adminConfig.MaxSize,
		systemVolumeSize: adminConfig.SystemVolumeSize,
		component:        consts.LabelValComponentShared,
		metaLabels:       labels,
		tags:             tags,
		vswitches:        vswitches,
	}); err != nil {
		return deploy.PanicOrDone(err)
	}

	// Create additional admin node pools with minSize=0 for each extra instance type
	for i, instanceType := range adminConfig.AdditionalInstanceTypes {
		altName := t.additionalNodePoolName(i)
		if err := t.createNodePool(ctx, nodePoolCreateParams{
			name:             altName,
			instanceType:     instanceType,
			minSize:          0,
			maxSize:          adminConfig.MaxSize,
			systemVolumeSize: adminConfig.SystemVolumeSize,
			component:        consts.LabelValComponentShared,
			metaLabels:       labels,
			tags:             tags,
			vswitches:        vswitches,
		}); err != nil {
			return deploy.Panic(fmt.Errorf("failed to create additional admin node pool for instance type %s: %w", instanceType, err))
		}
	}

	o11yPools, err := config.GetO11yNodePools(
		t.Config.APPBoxConfig.O11yNodeConfig,
		0, // default minSize=0 for o11y
		defaultNodeMaxSize,
		defaultSystemDiskSize,
	)
	if err != nil {
		return deploy.Error(err)
	}
	for _, pool := range o11yPools {
		o11yName := t.o11yNodePoolName(pool.InstanceType)
		desired[o11yName] = struct{}{}
		if err := t.createNodePool(ctx, nodePoolCreateParams{
			name:             o11yName,
			instanceType:     pool.InstanceType,
			minSize:          pool.MinSize,
			maxSize:          pool.MaxSize,
			systemVolumeSize: pool.SystemVolumeSize,
			component:        consts.LabelValComponentO11y,
			taints: []ackv1alpha1.KubernetesNodePoolTaintsParameters{{
				Effect: ptr.To(consts.TaintEffectNoScheduleForAliyun),
				Key:    ptr.To(consts.TaintKeyDedicated),
				Value:  ptr.To(consts.TaintValueO11y),
			}},
			metaLabels: labels,
			tags:       tags,
			vswitches:  vswitches,
			extraNodeLabels: map[string]string{
				consts.LabelKeyNodeSpec: nodepool.NodeSpecLabel(uint32(pool.CPU), uint32(pool.MemoryGi)),
			},
		}); err != nil {
			return deploy.Panic(fmt.Errorf("failed to create o11y node pool for instance type %s: %w", pool.InstanceType, err))
		}
	}

	if err := t.cleanupStaleAdminNodePools(ctx, desired); err != nil {
		return deploy.Error(fmt.Errorf("failed to clean up stale admin node pools: %w", err))
	}

	return deploy.Done()
}

// nodePoolCreateParams carries per-pool settings for admin and o11y node pools.
type nodePoolCreateParams struct {
	name             string
	instanceType     string
	minSize          int
	maxSize          int
	systemVolumeSize int64
	component        string
	taints           []ackv1alpha1.KubernetesNodePoolTaintsParameters
	metaLabels       map[string]string
	tags             map[string]string
	vswitches        []string
	// extraNodeLabels are appended to ACK node Labels after component / k8s-version.
	// Admin paths leave this nil.
	extraNodeLabels map[string]string
}

// createNodePool creates an ACK node pool (admin or o11y) with the given params.
func (t *nodePool) createNodePool(ctx context.Context, p nodePoolCreateParams) error {
	// Merge owner labels so all APPBox-owned node pool resources can be found by label selector.
	metaLabels := util.MergeMaps(p.metaLabels, labelutil.NewOwnerLabels(t.APPBox))
	np := &ackv1alpha1.KubernetesNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name:   p.name,
			Labels: metaLabels,
		},
		Spec: ackv1alpha1.KubernetesNodePoolSpec{
			ForProvider: ackv1alpha1.KubernetesNodePoolParameters{
				NodePoolName:  ptr.To(p.name),
				ClusterIDRefs: &xpv1.Reference{Name: t.Cluster.GetName()},
				InstanceTypes: []*string{ptr.To(p.instanceType)},
				RAMRoleName:   ptr.To(t.APPBox.Status.IAM.NodeRoleName),
				VswitchIds:    util.ToSlicePointer(p.vswitches),
				ScalingConfig: []ackv1alpha1.ScalingConfigParameters{
					{
						MaxSize: ptr.To(float64(p.maxSize)),
						MinSize: ptr.To(float64(p.minSize)),
					},
				},
				SystemDiskCategory: ptr.To(defaultSystemDiskCategory),
				SystemDiskSize:     ptr.To(float64(p.systemVolumeSize)),
				Tags:               util.ToMapPointer(p.tags),
				Taints:             p.taints,
				Labels:             buildACKNodeLabels(p.component, t.APPBox.Spec.KubernetesConfig.Version, p.extraNodeLabels),
			},
			ResourceSpec: xpv1.ResourceSpec{
				ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
				ManagementPolicies:      controllers.GetResourceManagementPolicies(),
			},
		},
	}
	return k8s.Apply(ctx, t.Local, np)
}

func buildACKNodeLabels(component, k8sVersion string, extra map[string]string) []ackv1alpha1.LabelsParameters {
	labels := []ackv1alpha1.LabelsParameters{
		{
			Key:   ptr.To(consts.LabelKeyComponent),
			Value: ptr.To(component),
		},
		{
			Key:   ptr.To(consts.K8sVersionLabelKey),
			Value: ptr.To(k8sVersion),
		},
	}
	for k, v := range extra {
		labels = append(labels, ackv1alpha1.LabelsParameters{
			Key:   ptr.To(k),
			Value: ptr.To(v),
		})
	}
	return labels
}

// cleanupStaleAdminNodePools deletes APPBox-owned node pools that are NOT in the desired set
// (admin primary/additional + o11y when enabled).
// If desired is nil, all such pools owned by this APPBox are deleted.
func (t *nodePool) cleanupStaleAdminNodePools(ctx context.Context, desired map[string]struct{}) error {
	nps := &ackv1alpha1.KubernetesNodePoolList{}
	if err := t.Local.List(ctx, nps, client.MatchingLabels(labels.NewOwnerLabels(t.APPBox))); err != nil {
		return err
	}
	for i := range nps.Items {
		np := &nps.Items[i]
		if desired != nil {
			if _, ok := desired[np.Name]; ok {
				continue
			}
		}
		if err := controllers.DeleteUpboundResource(ctx, t.Local, np); err != nil {
			return err
		}
	}
	return nil
}

func (t *nodePool) PostCreate(ctx context.Context) deploy.Result {
	if err := t.waitNodePoolReady(ctx, t.nodePoolName(), "AdminNodePoolNotReady"); err != nil {
		return deploy.Panic(err)
	}

	o11yPools, err := config.GetO11yNodePools(
		t.Config.APPBoxConfig.O11yNodeConfig,
		0, defaultNodeMaxSize, defaultSystemDiskSize,
	)
	if err != nil {
		return deploy.Panic(err)
	}
	for _, pool := range o11yPools {
		name := t.o11yNodePoolName(pool.InstanceType)
		if err := t.waitNodePoolReady(ctx, name, "O11yNodePoolNotReady"); err != nil {
			return deploy.Panic(err)
		}
	}

	t.APPBox.Status.Conditions.SetCondition(metav1.Condition{
		Type:               v1alpha1.APPBoxConditionK8SReady,
		Status:             metav1.ConditionTrue,
		Reason:             "NodePoolReady",
		Message:            "ACK admin/o11y node pools are ready",
		LastTransitionTime: metav1.Now(),
	})
	return deploy.Done()
}

func (t *nodePool) waitNodePoolReady(ctx context.Context, name, reason string) error {
	np := &ackv1alpha1.KubernetesNodePool{
		ObjectMeta: metav1.ObjectMeta{
			Name: name,
		},
	}
	if err := t.Local.Get(ctx, client.ObjectKeyFromObject(np), np); err != nil {
		return err
	}
	condition := np.Status.GetCondition(xpv1.TypeReady)
	if condition.Type == xpv1.TypeReady && condition.Status == corev1.ConditionTrue {
		return nil
	}
	t.APPBox.Status.Conditions.SetCondition(metav1.Condition{
		Type:               v1alpha1.APPBoxConditionK8SReady,
		Status:             metav1.ConditionStatus(condition.Status),
		Reason:             reason,
		Message:            fmt.Sprintf("ACK node pool %s is not ready", name),
		LastTransitionTime: metav1.Now(),
	})
	return fmt.Errorf("waiting for ACK node pool %s to be ready", name)
}

func (t *nodePool) Delete(ctx context.Context) deploy.Result {
	// Delete all admin node pools by label selector (not by names derived from config).
	// This handles the case where AdditionalInstanceTypes was changed before deletion.
	if err := t.cleanupStaleAdminNodePools(ctx, nil); err != nil {
		return deploy.Panic(err)
	}
	return deploy.Done()
}

func (t *nodePool) PostDelete(ctx context.Context) deploy.Result {
	// Check all admin node pools are gone by label selector (not by names from config).
	nps := &ackv1alpha1.KubernetesNodePoolList{}
	if err := t.Local.List(ctx, nps, client.MatchingLabels(labels.NewOwnerLabels(t.APPBox))); err != nil {
		return deploy.PanicOrDone(err)
	}
	if len(nps.Items) > 0 {
		return deploy.PanicOrDone(fmt.Errorf("admin node pools still exist: %d remaining", len(nps.Items)))
	}
	return deploy.Done()
}

func (t *nodePool) nodePoolName() string {
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(t.Config.ResourceNamePrefix))
}

func (t *nodePool) additionalNodePoolName(index int) string {
	prefix := fmt.Sprintf("%s-alt%s", t.Config.ResourceNamePrefix, strconv.Itoa(index))
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(prefix))
}

// o11yNodePoolName returns tidbcloud-o11y-{instanceType}-...
// (ResourceNamePrefix + "-o11y-" + instanceType with '.' replaced by '-').
func (t *nodePool) o11yNodePoolName(instanceType string) string {
	instanceTypePart := strings.ReplaceAll(instanceType, ".", "-")
	prefix := fmt.Sprintf("%s-o11y-%s", t.Config.ResourceNamePrefix, instanceTypePart)
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(prefix))
}
