package eks

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

	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	eksv1beta1 "github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	cpawsv1beta1 "github.com/crossplane-contrib/provider-aws/apis/v1beta1"
	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/go-logr/logr"
	upboundeksv1beta1 "github.com/upbound/provider-aws/apis/eks/v1beta1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/utils/pointer"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
)

const (
	maxUnavailablePercentage = "maxUnavailablePercentage"
	bottlerocketSuffix       = "-blt"
)

type nodeGroupCR interface {
	Object() client.Object

	GetName() string
	GetVersion() string
	GetStatus() string
	GetConditions() []commonv1.Condition

	SetVersion(version string)
	Apply(ctx context.Context, cli client.Client) error
	Delete(ctx context.Context, cli client.Client) error
}

type contribNodeGroup struct {
	ng *manualv1alpha1.NodeGroup
}

func (n *contribNodeGroup) Object() client.Object { return n.ng }
func (n *contribNodeGroup) GetName() string       { return n.ng.GetName() }
func (n *contribNodeGroup) GetVersion() string {
	if n.ng.Status.AtProvider.Version != "" {
		return n.ng.Status.AtProvider.Version
	}
	return ptr.Deref(n.ng.Spec.ForProvider.Version, "")
}
func (n *contribNodeGroup) GetStatus() string { return string(n.ng.Status.AtProvider.Status) }
func (n *contribNodeGroup) GetConditions() []commonv1.Condition {
	return n.ng.Status.Conditions
}
func (n *contribNodeGroup) SetVersion(version string) {
	n.ng.Spec.ForProvider.Version = pointer.String(version)
	n.ng.Spec.ForProvider.ReleaseVersion = nil
}
func (n *contribNodeGroup) Delete(ctx context.Context, cli client.Client) error {
	return k8s.DeleteObject(ctx, cli, n.ng)
}
func (n *contribNodeGroup) Apply(ctx context.Context, cli client.Client) error {
	return cli.Update(ctx, n.ng)
}

type upboundNodeGroup struct {
	ng *upboundeksv1beta1.NodeGroup
}

func (n *upboundNodeGroup) Object() client.Object { return n.ng }
func (n *upboundNodeGroup) GetName() string       { return n.ng.GetName() }
func (n *upboundNodeGroup) GetVersion() string {
	// Prefer status version if available.
	if v := ptr.Deref(n.ng.Status.AtProvider.Version, ""); v != "" {
		return v
	}
	return ptr.Deref(n.ng.Spec.ForProvider.Version, "")
}
func (n *upboundNodeGroup) GetStatus() string {
	return ptr.Deref(n.ng.Status.AtProvider.Status, "")
}
func (n *upboundNodeGroup) GetConditions() []commonv1.Condition {
	return n.ng.Status.Conditions
}
func (n *upboundNodeGroup) SetVersion(version string) {
	n.ng.Spec.ForProvider.Version = pointer.String(version)
	n.ng.Spec.ForProvider.ReleaseVersion = nil
}
func (n *upboundNodeGroup) Delete(ctx context.Context, cli client.Client) error {
	return k8s.DeleteObject(ctx, cli, n.ng)
}
func (n *upboundNodeGroup) Apply(ctx context.Context, cli client.Client) error {
	return cli.Update(ctx, n.ng)
}

func isControlPlaneUpgraded(eks *eksv1beta1.Cluster, desiredVer, currentVer string) bool {
	return eks.Status.AtProvider.Status == eksv1beta1.ClusterStatusActive &&
		desiredVer != "" && desiredVer == currentVer && desiredVer == eks.Status.AtProvider.Version
}

func getEKSCR(ctx context.Context, cli client.Client, appBox *v1alpha1.APPBox) (*eksv1beta1.Cluster, error) {
	var eksList eksv1beta1.ClusterList
	if err := cli.List(ctx, &eksList, client.MatchingLabelsSelector{
		Selector: labels.SelectorFromSet(map[string]string{
			namer.LegacyNetworkNamespaceSelectorKey: appBox.Namespace,
			namer.AppBoxSelectorKey:                 appBox.Name,
		}),
	}); err != nil {
		return nil, fmt.Errorf("failed to list eks clusters for appbox %s/%s: %w", appBox.Namespace, appBox.Name, err)
	}
	if len(eksList.Items) != 1 {
		return nil, fmt.Errorf("expected to find 1 eks cluster for appbox %s/%s, but found %d", appBox.Namespace, appBox.Name, len(eksList.Items))
	}
	return &eksList.Items[0], nil
}

func fetchNodeGroupCR(ctx context.Context, cli client.Client, name string) (nodeGroupCR, error) {
	var ng manualv1alpha1.NodeGroup
	if err := cli.Get(ctx, client.ObjectKey{Name: name}, &ng); err == nil {
		return &contribNodeGroup{ng: &ng}, nil
	} else if !apierrors.IsNotFound(err) {
		return nil, fmt.Errorf("failed to get nodegroup %s from crossplane-contrib: %w", name, err)
	}

	var upNG upboundeksv1beta1.NodeGroup
	if err := cli.Get(ctx, client.ObjectKey{Name: name}, &upNG); err != nil {
		if apierrors.IsNotFound(err) {
			return nil, fmt.Errorf("nodegroup %s not found in either crossplane-contrib or upbound provider", name)
		}
		return nil, fmt.Errorf("failed to get nodegroup %s from upbound provider: %w", name, err)
	}
	return &upboundNodeGroup{ng: &upNG}, nil
}

func getAssumedRoleFromProviderConfig(ctx context.Context, cli client.Client, providerConfigName string) (*aws.AssumedRole, error) {
	var pc cpawsv1beta1.ProviderConfig
	if err := cli.Get(ctx, client.ObjectKey{Name: providerConfigName}, &pc); err != nil {
		return nil, fmt.Errorf("failed to get providerConfig %s from crossplane-contrib: %w", providerConfigName, err)
	}

	if pc.Spec.AssumeRoleARN == nil || *pc.Spec.AssumeRoleARN == "" {
		return nil, fmt.Errorf("providerConfig %s has empty Spec.AssumeRoleARN", providerConfigName)
	}
	assumeRole := &aws.AssumedRole{RoleARN: ptr.Deref(pc.Spec.AssumeRoleARN, "")}
	if pc.Spec.ExternalID != nil {
		assumeRole.ExternalID = pc.Spec.ExternalID
	}
	return assumeRole, nil
}

// updateNodeGroupVersion updates the node group CR's version and release version.
func updateNodeGroupVersion(ctx context.Context, infraCli client.Client, ng *manualv1alpha1.NodeGroup, desiredVersion string) error {
	ng.Spec.ForProvider.Version = pointer.String(desiredVersion)
	ng.Spec.ForProvider.ReleaseVersion = nil
	return infraCli.Update(ctx, ng)
}

// setNodeGroupUpdateConfig configures the maximum number of nodes that can be unavailable during
// a node group update based on a percentage value provided in the options.
//
// Parameters:
//   - ng: The EKS node group to configure
//   - options: A map of configuration options, where the key "maxUnavailablePercentage" specifies
//     the percentage of nodes that can be unavailable during an update
//
// Returns:
//   - needUpdate: true if the node group's configuration was changed, false otherwise
//
// The function calculates the actual number of nodes that can be unavailable based on the
// current desired size of the node group and the specified percentage. It uses MaxUnavailable
// instead of MaxUnavailablePercentage due to a known issue in the Crossplane provider.
func setNodeGroupUpdateConfig(ng *manualv1alpha1.NodeGroup, options map[string]string) (needUpdate bool) {
	// Extract the maxUnavailablePercentage value from options
	p, ok := options[maxUnavailablePercentage]
	if !ok || p == "" {
		return false // No update needed if the option is not provided
	}

	// Get the current node count from the node group's scaling configuration
	nodeCount := ptr.Deref(ng.Status.AtProvider.ScalingConfig.DesiredSize, 0)

	// Parse the percentage value and validate it
	percentage, err := strconv.ParseFloat(p, 32)
	if nodeCount == 0 || err != nil || percentage <= 0 || percentage > 100 {
		return false // No update needed if validation fails
	}

	// Get the current MaxUnavailable value if UpdateConfig exists
	var curMaxUnavailable *int32
	if ng.Spec.ForProvider.UpdateConfig != nil {
		curMaxUnavailable = ng.Spec.ForProvider.UpdateConfig.MaxUnavailable
	}

	// Calculate the expected MaxUnavailable value based on the percentage
	expectMaxUnavailable := int32(float64(nodeCount) * 0.01 * percentage)

	// No update needed if the current value matches the expected value
	if curMaxUnavailable != nil && *curMaxUnavailable == expectMaxUnavailable {
		return false
	}

	// Ensure UpdateConfig exists before setting MaxUnavailable
	if ng.Spec.ForProvider.UpdateConfig == nil {
		ng.Spec.ForProvider.UpdateConfig = &manualv1alpha1.NodeGroupUpdateConfig{}
	}

	// It seems crossplane has a bug, if we set `MaxUnavailablePercentage`, it will report:
	// "Either provide max-unavailable or max-unavailable-percentage in the request."
	// So we use `MaxUnavailable` instead.
	ng.Spec.ForProvider.UpdateConfig.MaxUnavailable = ptr.To(expectMaxUnavailable)
	return true // Configuration was updated
}

// resetNodeGroupUpdateConfig resets the node group's update configuration to allow only one node to be unavailable during an update.
func resetNodeGroupUpdateConfig(ctx context.Context, infraCli client.Client, ng *manualv1alpha1.NodeGroup) error {
	if ng.Spec.ForProvider.UpdateConfig == nil {
		ng.Spec.ForProvider.UpdateConfig = &manualv1alpha1.NodeGroupUpdateConfig{}
	}
	ng.Spec.ForProvider.UpdateConfig.MaxUnavailable = ptr.To(int32(1))
	return infraCli.Update(ctx, ng)
}

func deleteAndWait(ctx context.Context, cli client.Client, obj client.Object, timeout time.Duration) error {
	if err := k8s.DeleteObject(ctx, cli, obj); err != nil {
		return fmt.Errorf("failed to delete object: %w", err)
	}
	retryInterval := 5 * time.Second
	deadline := time.Now().Add(timeout)

	for {
		// Check if the timeout has been exceeded
		if time.Now().After(deadline) {
			return fmt.Errorf("timeout exceeded while waiting for object deletion")
		}

		// Attempt to get the object
		if err := cli.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil {
			if client.IgnoreNotFound(err) == nil {
				return nil // Object successfully deleted
			}
			return fmt.Errorf("error while checking object deletion: %w", err)
		}

		// Wait for the retry interval or context cancellation
		select {
		case <-ctx.Done():
			return fmt.Errorf("context canceled while waiting for object deletion: %w", ctx.Err())
		case <-time.After(retryInterval):
		}
	}
}

// isTriggerTimeExceeded checks if the trigger time exceeds the given threshold.
// Logs an error if the trigger time is invalid.
//
// Parameters:
// - triggerVal: The trigger time value as a string.
// - threshold: The duration threshold to check against.
// - logger: The logger for logging errors.
// - addonName: The name of the addon for logging context.
//
// Returns:
// - true if the trigger time exceeds the threshold, false otherwise.
func isTriggerTimeExceeded(triggerVal string, threshold time.Duration, logger logr.Logger, addonName string) bool {
	triggerTime, err := time.Parse(time.RFC3339, triggerVal)
	if err != nil {
		logger.Error(err, "failed to parse trigger time", "addon", addonName)
		return true
	}
	return time.Since(triggerTime) > threshold
}
