package cluster

import (
	"fmt"
	"sort"

	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	corev1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/equality"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"sigs.k8s.io/controller-runtime/pkg/client"

	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
)

func (r *reconcileContext) updateProgress(phase cloudv1.ClusterPhase, reason string) error {
	if r.cluster.Status.Phase == phase && r.cluster.Status.Reason == reason {
		return nil
	}

	if err := r.ensureStatus(func() error {
		r.cluster.Status.Phase = phase
		r.cluster.Status.Reason = reason
		return nil
	}); err != nil {
		return fmt.Errorf("update progress err: %w", err)
	}
	r.InfraRecorder.Event(r.cluster, corev1.EventTypeNormal, string(phase), reason)

	return nil
}

// ensureStatus update the cluster status if necessary.
func (r *reconcileContext) ensureStatus(mutateFn func() error) error {
	var err error
	beforeMutation := r.cluster.Status.DeepCopy()
	if err = mutateFn(); err != nil {
		return err
	}
	if equality.Semantic.DeepEqual(beforeMutation, r.cluster.Status) {
		// no changed, skip updating
		// Note: the local status may not be same as the truth, but we will reach eventually-consistency via reconciliation
		return nil
	}

	return r.InfraClient.Status().Update(r.ctx, r.cluster)
}

// GetTc get TidbCluster.
func (r *reconcileContext) GetTc(cached bool) (*v1alpha1.TidbCluster, error) {
	tc := &v1alpha1.TidbCluster{}
	if cached {
		return tc, r.Cache.Get(r.ctx, r.tcKey, tc)
	}

	return tc, r.Client.Get(r.ctx, r.tcKey, tc)
}

// GetTm get TidbMonitor.
func (r *reconcileContext) GetTm(cached bool) (*v1alpha1.TidbMonitor, error) {
	tm := &v1alpha1.TidbMonitor{}
	if cached {
		return tm, r.Cache.Get(r.ctx, r.tcKey, tm)
	}

	return tm, r.Client.Get(r.ctx, r.tcKey, tm)
}

// createWithOwnerLabel will create object with cluster owner labels.
// All of resources created by this controller should have these labels.
func (r *reconcileContext) createWithOwnerLabel(obj client.Object) error {
	obj.SetLabels(labels.NewOwnerLabels(r.cluster, obj.GetLabels()))
	return r.Client.Create(r.ctx, obj)
}

// createOrGetWithOwnerLabel will create object with cluster owner labels.
// If it is already exists, get this object
func (r *reconcileContext) createOrGetWithOwnerLabel(obj client.Object) error {
	if err := r.createWithOwnerLabel(obj); err != nil {
		if !apierrors.IsAlreadyExists(err) {
			return err
		}
		return r.Cache.Get(r.ctx, client.ObjectKeyFromObject(obj), obj)
	}

	return nil
}

// Tag is used as a common struct for all cloud tags
type Tag struct {
	Key   string
	Value string
}

type TagSlice []Tag

func (x TagSlice) Len() int           { return len(x) }
func (x TagSlice) Swap(i, j int)      { x[i], x[j] = x[j], x[i] }
func (x TagSlice) Less(i, j int) bool { return x[i].Key < x[j].Key }

func (r *reconcileContext) getTags(component, usedBy string) []Tag {
	tags := []Tag{
		{
			Key:   types.BillingTagKeyServiceType,
			Value: types.BillingTagValServiceTypeSharedTier,
		},
		{
			Key:   types.BillingTagKeyEnvironment,
			Value: r.Env,
		},
		{
			Key:   types.BillingTagKeyUsedBy,
			Value: usedBy,
		},
		{
			Key:   types.BillingTagKeyComponent,
			Value: component,
		},
	}

	for k, v := range r.tags {
		tags = append(tags, Tag{
			Key:   k,
			Value: v,
		})
	}

	sort.Sort(TagSlice(tags))

	return tags
}
