// Add cloud-agnostic helper functions here.

package common

import (
	"context"
	"fmt"

	"github.com/crossplane-contrib/provider-aws/apis/autoscaling/v1beta1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/manualv1alpha1"
	gkev1beta1 "github.com/crossplane-contrib/provider-gcp/apis/container/v1beta1"
	commonv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
	"github.com/go-logr/logr"
	opv1alpha1 "github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	appsv1 "k8s.io/api/apps/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	"k8s.io/utils/pointer"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/eks-provider/controllers"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
)

func ExpectToBeUpgraded(target cloudv1.UpgradeOperationTarget, desiredVer, curVer string) bool {
	skew := map[cloudv1.UpgradeOperationTarget]int{
		cloudv1.ControlPlane: 1,
		cloudv1.AdminNodes:   2,
		cloudv1.DMNodes:      2,
		cloudv1.CDCNodes:     2,
	}[target]

	var desiredMajor, desiredMinor, curMajor, curMinor int
	if _, err := fmt.Sscanf(desiredVer, "%d.%d", &desiredMajor, &desiredMinor); err != nil {
		return false
	}
	if _, err := fmt.Sscanf(curVer, "%d.%d", &curMajor, &curMinor); err != nil {
		return false
	}
	return desiredMinor-curMinor == skew
}

func GetTiDBNodeVersion(cluster *cloudv1.Cluster, providerType string) (currentVersion string, err error) {
	switch providerType {
	case consts.ProviderTypeEKS:
		status, e := cloudv1.EKSClusterStatusFromCluster(cluster)
		if e != nil {
			err = e
			break
		}
		currentVersion = status.NodeVersion
	case consts.ProviderTypeGKE:
		status, e := cloudv1.GKEClusterStatusFromCluster(cluster)
		if e != nil {
			err = e
			break
		}
		currentVersion = status.NodeVersion
	}
	return
}

// MutateTCForRestarting add annotations to component's spec to restarts the tidb cluster gracefully.
// According to the doc, need to add an annotation like:
//
// `tidb.pingcap.com/restartedAt: 2020-04-20T12:00`
//
// To avoid the annotation being modified multiple times, we use the desired version as the value.
func MutateTCForRestarting(tc *opv1alpha1.TidbCluster, version string) (changed bool) {
	temp := tc.DeepCopy()

	if tc.Spec.PD != nil {
		setRestartAnnotation(&tc.Spec.PD.ComponentSpec, version)
	}
	if tc.Spec.TiKV != nil {
		setRestartAnnotation(&tc.Spec.TiKV.ComponentSpec, version)
	}
	if tc.Spec.TiDB != nil {
		setRestartAnnotation(&tc.Spec.TiDB.ComponentSpec, version)
	}
	if tc.Spec.TiFlash != nil {
		setRestartAnnotation(&tc.Spec.TiFlash.ComponentSpec, version)
	}
	if tc.Spec.TiProxy != nil {
		setRestartAnnotation(&tc.Spec.TiProxy.ComponentSpec, version)
	}

	return !equality.Semantic.DeepEqual(tc.Spec, temp.Spec)
}

func setRestartAnnotation(compSpec *opv1alpha1.ComponentSpec, version string) {
	if compSpec.Annotations == nil {
		compSpec.Annotations = map[string]string{}
	}
	compSpec.Annotations[RestartTiDBAnnotation] = version
}

func PauseNetworkAndCluster(ctx context.Context, cli client.Client, network *cloudv1.Network, cluster *cloudv1.Cluster) (updated bool, err error) {
	if network.Status.Phase != cloudv1.NetworkPhaseNormal {
		return false, fmt.Errorf("network is not normal, network: %s, namespace: %s, phase: %s", network.Name, network.Namespace, network.Status.Phase)
	}
	if network != nil && (network.Spec.Paused == nil || !*network.Spec.Paused) {
		network.Spec.Paused = pointer.Bool(true)
		if err := k8s.Apply(ctx, cli, network); err != nil {
			return false, fmt.Errorf("failed to pause network: %w", err)
		}
		updated = true
	}

	if cluster == nil {
		return updated, nil
	}
	if cluster.Status.Phase != cloudv1.ClusterPhaseRunning && cluster.Status.Phase != cloudv1.ClusterPhaseSuspend {
		return updated, fmt.Errorf("cluster is not ready for upgrading, cluster: %s, namespace: %s, phase: %s", cluster.Name, cluster.Namespace, cluster.Status.Phase)
	}
	if cluster.Spec.Paused == nil || !*cluster.Spec.Paused {
		cluster.Spec.Paused = pointer.Bool(true)
		if err := k8s.Apply(ctx, cli, cluster); err != nil {
			return updated, fmt.Errorf("failed to pause cluster: %w", err)
		}
		updated = true
	}

	return updated, nil
}

// UnpauseNetworkAndClusters unpauses the network CR's reconcile and all related cluster CRs mentioned in the UpgradeOperation's spec.
func UnpauseNetworkAndClusters(ctx context.Context, cli client.Client, uo *cloudv1.UpgradeOperation, network *cloudv1.Network) error {
	// Do not unpause migrated CRs.
	if controllers.IsMigratedCR(network) {
		return nil
	}

	if network != nil && network.Spec.Paused != nil && *network.Spec.Paused {
		network.Spec.Paused = pointer.Bool(false)
		if err := k8s.Apply(ctx, cli, network); err != nil {
			return fmt.Errorf("failed to unpause network: %w", err)
		}
	}

	for _, step := range uo.Spec.Steps {
		if step.Cluster != "" {
			var cluster cloudv1.Cluster
			if err := cli.Get(ctx, client.ObjectKey{Name: step.Cluster, Namespace: uo.Namespace}, &cluster); err != nil {
				return fmt.Errorf("failed to get cluster %s/%s: %w", uo.Namespace, step.Cluster, err)
			}
			if cluster.Spec.Paused != nil && *cluster.Spec.Paused {
				cluster.Spec.Paused = pointer.Bool(false)
				if err := k8s.Apply(ctx, cli, &cluster); err != nil {
					return fmt.Errorf("failed to unpause cluster: %w", err)
				}
			}
		}
	}

	return nil
}

// FinishUpgrading unpauses network and cluster CRs' reconcile and updates UpgradeOperation's status.
func FinishUpgrading(ctx context.Context, cli client.Client, uo *cloudv1.UpgradeOperation) error {
	uo.FinishCurrentStep()
	return cli.Status().Update(ctx, uo)
}

func UpdateDeploymentReplicas(ctx context.Context, cli client.Client, namespace, name string, replicas int32) error {
	deploy := &appsv1.Deployment{}
	if err := cli.Get(ctx, client.ObjectKey{Name: name, Namespace: namespace}, deploy); err != nil {
		return err
	}

	deploy.Spec.Replicas = pointer.Int32(replicas)
	return k8s.Apply(ctx, cli, deploy)
}

// IsCRReadyAndSynced checks if the crossplane CR is ready and synced.
func IsCRReadyAndSynced(conditions []commonv1.Condition) bool {
	ready, synced := false, false

	for _, c := range conditions {
		switch c.Type {
		case commonv1.TypeReady:
			ready = c.Status == corev1.ConditionTrue
		case commonv1.TypeSynced:
			synced = c.Status == corev1.ConditionTrue
		}
	}

	return ready && synced
}

func IsTCReady(tc *opv1alpha1.TidbCluster) bool {
	for _, cond := range tc.Status.Conditions {
		if cond.Type == opv1alpha1.TidbClusterReady && cond.Status == corev1.ConditionTrue {
			return true
		}
	}
	return false
}

func UpdateClusterStatus(ctx context.Context, infraCli client.Client, cluster *cloudv1.Cluster, desiredVersion string) error {
	if err := cloudv1.UpdateClusterStatus(cluster, func(s *cloudv1.CommonK8sClusterStatus) bool {
		if s.NodeVersion == desiredVersion {
			return false
		}
		s.NodeVersion = desiredVersion
		return true
	}); err != nil {
		return fmt.Errorf("failed to update cluster CR status's node version: %w", err)
	}

	return infraCli.Status().Update(ctx, cluster)
}

func GetCRNameList(objects interface{}) []string {
	var names []string

	switch objs := objects.(type) {
	case []*manualv1alpha1.NodeGroup:
		for _, o := range objs {
			names = append(names, o.GetName())
		}
	case []*v1beta1.AutoScalingGroup:
		for _, o := range objs {
			names = append(names, o.GetName())
		}
	case []*gkev1beta1.NodePool:
		for _, o := range objs {
			names = append(names, o.GetName())
		}
	}

	return names
}

// LogAndEmitEvent logs the event and emits it using the provided logger and eventRecorder.
// The event type determines how the event is logged.
func LogAndEmitEvent(logger logr.Logger, eventRecorder record.EventRecorder, obj runtime.Object, eventType, msg string, err error) {
	switch eventType {
	case NormalEvent:
		logger.Info(msg)

	case WarningEvent:
		logger.Error(err, msg)
	}
	eventRecorder.Event(obj, eventType, "", msg)
}
