package regionaction

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

	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	"k8s.io/apimachinery/pkg/util/wait"
	"k8s.io/utils/ptr"
	"sigs.k8s.io/controller-runtime/pkg/client"

	entity "github.com/tidbcloud/devops/entity/k8supgrade"
	opent "github.com/tidbcloud/devops/entity/operation"
	"github.com/tidbcloud/devops/pkg/service/operation/actions"
)

const (
	// Default retry settings for nodegroup updates
	maxRetries        = 5
	initialBackoff    = 1 * time.Second
	maxBackoff        = 30 * time.Second
	backoffMultiplier = 2.0
)

var (
	ActionUpgradeCDCNodes = opent.ActionMetadata{
		Name: "UpgradeCDCNodes",
		Type: opent.ActionTypeAuto,
	}

	backoff = wait.Backoff{
		Steps:    maxRetries,
		Duration: initialBackoff,
		Factor:   backoffMultiplier,
		Cap:      maxBackoff,
	}
)

type upgradeCDCNodes struct {
	actions.BaseAction
	comps *actions.Components
}

func NewUpgradeCDCNodes(comps *actions.Components) actions.AutoAction {
	return &upgradeCDCNodes{comps: comps}
}

func (a *upgradeCDCNodes) Metadata() opent.ActionMetadata {
	return ActionUpgradeCDCNodes
}

func (a *upgradeCDCNodes) PreCheck(_ context.Context, _ *opent.Instance) error {
	return nil
}

func (a *upgradeCDCNodes) Start(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	cidStr, _ := instance.GetParam(opent.ParamClusterID)
	if cidStr == "" {
		return actions.FailedActionResult(fmt.Errorf("cluster_id is required")), nil
	}
	cid, err := strconv.ParseUint(cidStr, 10, 64)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}

	info, err := getK8sInformation(ctx, a.comps, cid, cidStr)
	if err != nil {
		return actions.FailedActionResult(err), nil
	}
	a.comps.Logger.Info("get k8s information info", "info", info)

	desiredVersion, _ := instance.GetParam(opent.ParamDesiredVersion)
	if desiredVersion == "" {
		desiredVersion = info.ControlPlaneVersion
		a.comps.Logger.Info("desired version is not provided, will use the control plane version", "version", desiredVersion)
	}

	var ngList manualv1alpha1.NodeGroupList
	if err := a.comps.InfraAPICli.GetClient().List(ctx, &ngList, client.MatchingLabels{
		"component": "ticdc",
		"cluster":   cidStr,
	}); err != nil {
		a.comps.Logger.Error(err, "failed to list CDC nodegroups", "cluster_id", cid)
		return actions.FailedActionResult(fmt.Errorf("failed to list CDC nodegroups: %w", err)), nil
	}

	if len(ngList.Items) == 0 {
		a.comps.Logger.Info("no CDC nodegroups found, skipping upgrade", "cluster_id", cid)
		return actions.SucceededActionResult(nil), nil
	}

	a.comps.Logger.Info("found CDC nodegroups to upgrade", "cluster_id", cid, "count", len(ngList.Items))

	// Track nodegroup names for output
	ngNames := make([]string, 0, len(ngList.Items))
	for i := range ngList.Items {
		ng := &ngList.Items[i]
		ngNames = append(ngNames, ng.Name)

		if !needUpgrade(ng.Status.AtProvider.Version, desiredVersion) {
			a.comps.Logger.Info("no need to upgrade, skipping",
				"nodegroup", ng.Name,
				"current_version", ng.Status.AtProvider.Version,
				"desired_version", desiredVersion)
			continue
		}

		a.comps.Logger.Info("updating nodegroup version",
			"nodegroup", ng.Name,
			"current_version", ng.Status.AtProvider.Version,
			"desired_version", desiredVersion)

		// Implement retry logic for nodegroup update
		var updateErr error
		err := wait.ExponentialBackoff(backoff, func() (bool, error) {
			// Need to get the latest version of the nodegroup before updating
			latestNG := &manualv1alpha1.NodeGroup{}
			if err := a.comps.InfraAPICli.GetClient().Get(ctx, client.ObjectKey{Name: ng.Name, Namespace: ng.Namespace}, latestNG); err != nil {
				a.comps.Logger.Error(err, "failed to get latest nodegroup state",
					"nodegroup", ng.Name,
					"cluster_id", cid)
				updateErr = err
				return false, nil // Don't abort retries for this error
			}

			// update the nodegroup's version
			latestNG.Spec.ForProvider.Version = ptr.To(desiredVersion)
			latestNG.Spec.ForProvider.ReleaseVersion = nil
			if err := a.comps.InfraAPICli.GetClient().Update(ctx, latestNG); err != nil {
				a.comps.Logger.Error(err, "failed to update nodegroup, will retry",
					"nodegroup", ng.Name,
					"cluster_id", cid,
					"attempt", backoff.Steps-maxRetries+1)
				updateErr = err
				return false, nil // Don't abort retries
			}

			a.comps.Logger.Info("successfully updated nodegroup version",
				"nodegroup", ng.Name,
				"desired_version", desiredVersion)
			return true, nil
		})
		if err != nil {
			a.comps.Logger.Error(updateErr, "failed to update nodegroup after all retries",
				"nodegroup", ng.Name,
				"cluster_id", cid)
		}
	}

	// Create output with nodegroup names
	output := map[opent.OutputName]string{
		opent.OutputNodeGroupNames: strings.Join(ngNames, ","),
		opent.OutputDesiredVersion: desiredVersion,
	}
	return actions.RunningActionResult(fmt.Sprintf("started upgrade of %d CDC nodegroups to version %s", len(ngList.Items), desiredVersion), output), nil
}

func (a *upgradeCDCNodes) Result(ctx context.Context, instance *opent.Instance) (*actions.ActionResult, error) {
	desiredVersion := instance.Outputs[opent.OutputDesiredVersion]
	if desiredVersion == "" {
		return nil, fmt.Errorf("desired_version is required")
	}

	cidStr, _ := instance.GetParam(opent.ParamClusterID)
	if cidStr == "" {
		return actions.FailedActionResult(fmt.Errorf("cluster_id is required")), nil
	}

	var ngList manualv1alpha1.NodeGroupList
	if err := a.comps.InfraAPICli.GetClient().List(ctx, &ngList, client.MatchingLabels{
		"component": "ticdc",
		"cluster":   cidStr,
	}); err != nil {
		return actions.FailedActionResult(fmt.Errorf("failed to list nodegroups: %w", err)), nil
	}

	if len(ngList.Items) == 0 {
		return actions.SucceededActionResult(nil), nil
	}

	// Check if all nodegroups are at the desired version and active
	needRetry := false
	for i := range ngList.Items {
		ng := &ngList.Items[i]
		if !hasUpgraded(ng, desiredVersion) {
			return actions.RunningActionResult(fmt.Sprintf("ticdc nodegroup %s is not upgraded to %s or not active yet", ng.Name, desiredVersion), nil), nil
		} else {
			// reset the update config to default config
			// Note that set it to nil will not work
			ng.Spec.ForProvider.UpdateConfig = &manualv1alpha1.NodeGroupUpdateConfig{
				MaxUnavailable:           ptr.To(int32(1)),
				MaxUnavailablePercentage: nil,
				Force:                    ptr.To(false),
			}
			// After upgrade, the release version in spec may be outdated, and need to fix it.
			if ng.Spec.ForProvider.ReleaseVersion != nil && *ng.Spec.ForProvider.ReleaseVersion != ng.Status.AtProvider.ReleaseVersion {
				ng.Spec.ForProvider.ReleaseVersion = nil
			}
			if err := a.comps.InfraAPICli.GetClient().Update(ctx, ng); err != nil {
				a.comps.Logger.Error(err, "failed to reset UpdateConfig for ticdc nodegroup", "nodegroup", ng.Name)
				needRetry = true
			}
		}
	}

	if needRetry {
		return actions.RunningActionResult("retrying to reset UpdateConfig after upgrade", nil), nil
	}

	return actions.SucceededActionResult(nil), nil
}

// needUpgrade checks if the desired version is within 2 minor versions of the current version.
func needUpgrade(currentVersion, desiredVersion string) bool {
	current, desired := entity.NewMustVersion(currentVersion), entity.NewMustVersion(desiredVersion)
	skew := desired.Sub(current)
	return skew <= 2 && skew > 0
}

// hasUpgraded checks if the node group has been upgraded successfully.
func hasUpgraded(ng *manualv1alpha1.NodeGroup, desiredVersion string) bool {
	return ng.Status.AtProvider.Version == desiredVersion &&
		ng.Status.AtProvider.Status == manualv1alpha1.NodeGroupStatusActive &&
		ng.Spec.ForProvider.ReleaseVersion != nil && *ng.Spec.ForProvider.ReleaseVersion == ng.Status.AtProvider.ReleaseVersion
}
