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"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8serrors "k8s.io/apimachinery/pkg/util/errors"
	"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/common/utils"
	"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/namer"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/util"
)

// recoverableErrorPatterns contains error message patterns that indicate
// the ACK cluster is in a transient state (e.g., updating) and addon installation
// should be retried after the cluster becomes stable.
var recoverableErrorPatterns = []string{
	"state is updating",
	"InvalidClusterStatus.Forbidden",
}

const (
	// maxRecoveryAttempts is the maximum number of recovery attempts for a failed addon
	maxRecoveryAttempts = 5

	// baseBackoffDuration is the base duration for exponential backoff between recovery attempts
	baseBackoffDuration = 30 * time.Second
)

type addons struct {
	*task.BaseTask
}

func NewAddons(c *task.Context) deploy.Task {
	return &addons{BaseTask: &task.BaseTask{Context: c}}
}

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

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

	addonConfigs, versionExists := t.AddonTmplConfig.ACK[t.APPBox.Spec.KubernetesConfig.Version]
	if !versionExists {
		return deploy.Error(fmt.Errorf("AddonTemplateNotFound for k8s version %s", t.APPBox.Spec.KubernetesConfig.Version))
	}
	labels := util.MergeMaps(t.Namer.Labels(), t.APPBox.Labels)
	errList := make([]error, 0)
	for _, addonConf := range addonConfigs {
		if _, ok := bootstrapAddons[addonConf.AddonName]; ok {
			continue // skip bootstrap addons
		}
		addonCRName := t.getAddonName(addonConf.AddonName)
		addon := &ackv1alpha1.KubernetesAddon{
			ObjectMeta: metav1.ObjectMeta{
				Name:   addonCRName,
				Labels: labels,
			},
			Spec: ackv1alpha1.KubernetesAddonSpec{
				ForProvider: ackv1alpha1.KubernetesAddonParameters{
					Name:          ptr.To(addonConf.AddonName),
					Version:       ptr.To(addonConf.AddonVersion),
					Region:        ptr.To(t.APPBox.Spec.Region),
					ClusterIDRefs: &xpv1.Reference{Name: t.Cluster.GetName()},
					Config:        addonConf.Values,
				},
				ResourceSpec: xpv1.ResourceSpec{
					ProviderConfigReference: &xpv1.Reference{Name: t.ProviderConfigName()},
					ManagementPolicies:      controllers.GetResourceManagementPolicies(),
				},
			},
		}
		if err := k8s.Apply(ctx, t.Local, addon); err != nil {
			errList = append(errList, fmt.Errorf("create or update addon %s: %v", addon.Name, err))
		}
	}
	return deploy.ErrorOrDone(k8serrors.NewAggregate(errList))
}

func (t *addons) PostCreate(ctx context.Context) deploy.Result {
	addonConfigs, exist := t.AddonTmplConfig.ACK[t.APPBox.Spec.KubernetesConfig.Version]
	if !exist {
		return deploy.Error(fmt.Errorf("AddonTemplateNotFound for k8s version %s", t.APPBox.Spec.KubernetesConfig.Version))
	}

	addonStatus := make([]v1alpha1.AddonStatus, 0, len(addonConfigs))
	for _, addonConf := range addonConfigs {
		if _, ok := bootstrapAddons[addonConf.AddonName]; ok {
			continue // skip bootstrap addons
		}
		addon := &ackv1alpha1.KubernetesAddon{
			ObjectMeta: metav1.ObjectMeta{
				Name: t.getAddonName(addonConf.AddonName),
			},
		}
		if err := t.Local.Get(ctx, client.ObjectKeyFromObject(addon), addon); err != nil {
			return deploy.Error(err)
		}

		// Check if addon sync failed and handle recovery
		if isRecoverableSyncError(addon.Status.Conditions) {
			if err := t.handleAddonSyncFailure(ctx, addon, addonConf.AddonName); err != nil {
				return deploy.Error(fmt.Errorf("failed to handle addon %s sync failed: %w", addonConf.AddonName, err))
			}
			// If handleAddonSyncFailure returns nil, addon was deleted for recovery
			// Return panic to trigger reconciliation
			return deploy.Panic(fmt.Errorf("addon %s deleted for recovery, waiting for recreation", addonConf.AddonName))
		}

		if !controllers.IsResourceReady(addon.Status.ResourceStatus) {
			return deploy.Error(fmt.Errorf("addon component %s is not active", addon.Name))
		}

		// Addon is ready, clear any recovery annotations
		t.clearRecoveryAnnotations(addonConf.AddonName)

		addonStatus = append(addonStatus, v1alpha1.AddonStatus{
			Name:    *addon.Spec.ForProvider.Name,
			Version: *addon.Spec.ForProvider.Version,
		})
	}
	t.APPBox.Status.Addons = addonStatus
	return deploy.Done()
}

func (t *addons) Delete(ctx context.Context) deploy.Result {
	addonList := new(ackv1alpha1.KubernetesAddonList)
	selector := client.MatchingLabels{namer.AppBoxSelectorKey: t.APPBox.Name}
	if err := t.Local.List(ctx, addonList, selector); err != nil {
		return deploy.Error(err)
	}
	for _, addon := range addonList.Items {
		if err := controllers.DeleteUpboundResource(ctx, t.Local, &addon); err != nil {
			return deploy.Error(err)
		}
	}

	return deploy.Done()
}

func (t *addons) getAddonName(name string) string {
	return controllers.UniqueIdentifier(t.APPBox.ObjectMeta, namer.WithPrefix(name), namer.WithPrefix(t.Config.ResourceNamePrefix))
}

// handleAddonSyncFailure handles addon sync failures with automatic recovery for transient errors.
func (t *addons) handleAddonSyncFailure(ctx context.Context, addon *ackv1alpha1.KubernetesAddon, addonName string) error {
	t.Logger.Info("Addon sync failed", "addon", addonName)

	// Get current recovery attempts
	attempts := t.getRecoveryAttempts(addonName)
	if attempts >= maxRecoveryAttempts {
		return fmt.Errorf("addon %s recovery failed after %d attempts", addonName, attempts)
	}

	// Check backoff time
	lastTimestamp := t.getRecoveryTimestamp(addonName)
	backoffDuration := t.calculateBackoff(attempts)
	if lastTimestamp > 0 {
		elapsed := time.Since(time.Unix(lastTimestamp, 0))
		if elapsed < backoffDuration {
			remaining := backoffDuration - elapsed
			t.Logger.Info("Addon recovery backoff not elapsed, waiting",
				"addon", addonName,
				"remaining", remaining.String())
			return fmt.Errorf("addon %s waiting for backoff: %v remaining", addonName, remaining)
		}
	}

	// Delete the failed addon CR to allow recreation
	t.Logger.Info("Deleting failed addon CR for recovery",
		"addon", addonName,
		"attempt", attempts+1,
		"backoff", backoffDuration.String())

	// Note: delete the addon CR without modifying the management policies
	if err := t.Local.Delete(ctx, addon); err != nil {
		return fmt.Errorf("failed to delete addon %s for recovery: %w", addonName, err)
	}

	// Update recovery annotations
	t.updateRecoveryAnnotations(addonName, attempts+1)
	return nil
}

// isRecoverableSyncError checks if the addon conditions indicate a recoverable sync failure.
// When installing the goat-scaler addon, the ack cluster may be in the updating state,
// which will cause other addon installation to fail with a message like "state is updating" or "InvalidClusterStatus.Forbidden".
// In this case, we can delete the failed addon CR to trigger a recreation to recovery.
func isRecoverableSyncError(conditions []xpv1.Condition) bool {
	failed, _ := utils.IsSyncFailed(conditions)
	if failed {
		failed2, lastOpErr := utils.IsLastAsyncOperationFailed(conditions)
		if failed2 {
			for _, pattern := range recoverableErrorPatterns {
				if strings.Contains(strings.ToLower(lastOpErr.Error()), strings.ToLower(pattern)) {
					return true
				}
			}
		}
	}
	return false
}

// getRecoveryAttempts returns the number of recovery attempts for an addon.
func (t *addons) getRecoveryAttempts(addonName string) int {
	if t.APPBox.Annotations == nil {
		return 0
	}
	key := consts.AnnoKeyACKAddonRecoveryAttempt(addonName)
	val, ok := t.APPBox.Annotations[key]
	if !ok {
		return 0
	}
	attempts, err := strconv.Atoi(val)
	if err != nil {
		t.Logger.Error(err, "Invalid addon recovery attempt annotation, disabling recovery",
			"addon", addonName,
			"key", key,
			"value", val)
		return maxRecoveryAttempts
	}
	if attempts < 0 {
		t.Logger.Error(fmt.Errorf("negative attempts: %d", attempts), "Invalid addon recovery attempt annotation, disabling recovery",
			"addon", addonName,
			"key", key,
			"value", val)
		return maxRecoveryAttempts
	}
	return attempts
}

// getRecoveryTimestamp returns the unix timestamp of the last recovery attempt.
func (t *addons) getRecoveryTimestamp(addonName string) int64 {
	if t.APPBox.Annotations == nil {
		return 0
	}
	key := consts.AnnoKeyACKAddonRecoveryTimestamp(addonName)
	val, ok := t.APPBox.Annotations[key]
	if !ok {
		return 0
	}
	ts, err := strconv.ParseInt(val, 10, 64)
	if err != nil {
		t.Logger.Error(err, "Invalid addon recovery timestamp annotation, enforcing backoff wait",
			"addon", addonName,
			"key", key,
			"value", val)
		return time.Now().Unix()
	}
	if ts < 0 {
		t.Logger.Error(fmt.Errorf("negative timestamp: %d", ts), "Invalid addon recovery timestamp annotation, enforcing backoff wait",
			"addon", addonName,
			"key", key,
			"value", val)
		return time.Now().Unix()
	}
	now := time.Now().Unix()
	if ts > now {
		t.Logger.Error(fmt.Errorf("timestamp is in the future: %d", ts), "Invalid addon recovery timestamp annotation, enforcing backoff wait",
			"addon", addonName,
			"key", key,
			"value", val)
		return now
	}
	return ts
}

// updateRecoveryAnnotations updates the recovery annotations for an addon.
func (t *addons) updateRecoveryAnnotations(addonName string, attempts int) {
	if t.APPBox.Annotations == nil {
		t.APPBox.Annotations = make(map[string]string)
	}
	attemptKey := consts.AnnoKeyACKAddonRecoveryAttempt(addonName)
	timestampKey := consts.AnnoKeyACKAddonRecoveryTimestamp(addonName)

	t.APPBox.Annotations[attemptKey] = strconv.Itoa(attempts)
	t.APPBox.Annotations[timestampKey] = strconv.FormatInt(time.Now().Unix(), 10)
}

// clearRecoveryAnnotations removes recovery annotations for an addon after successful installation.
func (t *addons) clearRecoveryAnnotations(addonName string) {
	if t.APPBox.Annotations == nil {
		return
	}
	attemptKey := consts.AnnoKeyACKAddonRecoveryAttempt(addonName)
	timestampKey := consts.AnnoKeyACKAddonRecoveryTimestamp(addonName)

	delete(t.APPBox.Annotations, attemptKey)
	delete(t.APPBox.Annotations, timestampKey)
}

// calculateBackoff returns the backoff duration for a given attempt number.
// Uses exponential backoff: 30s, 60s, 120s, etc.
func (t *addons) calculateBackoff(attempts int) time.Duration {
	if attempts <= 0 {
		return baseBackoffDuration
	}
	multiplier := 1 << attempts // 2^attempts
	return baseBackoffDuration * time.Duration(multiplier)
}
