package cluster

import (
	"context"
	"reflect"
	"time"

	"k8s.io/utils/ptr"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"

	"github.com/tidbcloud/eks-provider/controllers/cluster/task"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/metrics"
	"github.com/tidbcloud/infra-provider/pkg/o11y/tracing"
	"github.com/tidbcloud/infra-provider/pkg/util"
)

var (
	// requeueAfterOnClusterUnsynced is used for doing a forcible reconciliation if all tasks return Done
	// but the cluster is still in 'syncing' (scaling, pausing, etc) phase.
	requeueAfterOnClusterUnsynced = time.Second * 10
)

func exportReconcileErrorMetric(t *task.Context) {
	cluster := t.Cluster
	clusterVersion := ""
	if cluster.Status.DB != nil {
		clusterVersion = cluster.Status.DB.Version
	}
	clusterPaused := "false"
	if ptr.Deref(cluster.Spec.Paused, false) {
		clusterPaused = "true"
	}
	labels := map[string]string{
		"provider":   cluster.GetProviderType(),
		"region":     cluster.GetRegion(),
		"name":       cluster.Name,
		"namespace":  cluster.Namespace,
		"tenant_id":  cluster.GetTenantID(),
		"project_id": cluster.GetProjectID(),
		"version":    clusterVersion,
		"status":     string(cluster.Status.Phase),
		"paused":     clusterPaused,
	}

	metrics.ClusterReconcileError.With(labels).Add(float64(1))
}

func updateCreate(ctx context.Context, t *task.Context, results []deploy.Result) (res ctrl.Result, err error) {
	var retriableErr error
	res, err, retriableErr = deploy.MinRequeue(results)
	if retriableErr != nil {
		log.FromContext(ctx).Info("retriable error", "error", retriableErr)
	}
	defer func() {
		if err != nil {
			exportReconcileErrorMetric(t)
		}
	}()
	res = forceRequeueIfClusterIsUnsynced(ctx, t.Cluster, res)

	switch phase := t.Cluster.Status.Phase; phase {
	case cloudv1.ClusterPhasePending:
	case cloudv1.ClusterPhaseCreating:
		if err == nil && retriableErr == nil {
			age := time.Since(t.Cluster.CreationTimestamp.Time)
			log.FromContext(ctx).Info("mark cluster become running when creating cluster", "sinceCreate", age)

			t.Cluster.Status.Phase = cloudv1.ClusterPhaseRunning
			metrics.ClusterFirstPassStageSeconds.WithLabelValues("All").Observe(age.Seconds())
		}
	default:
	}

	switch t.Config.Provider {
	case consts.ProviderTypeEKS:
		if err := cloudv1.SetEKSClusterSpec(t.Cluster, t.EKSClusterSpec); err != nil {
			return res, err
		}
	case consts.ProviderTypeGKE:
		if err := cloudv1.SetGKEClusterSpec(t.Cluster, t.GKEClusterSpec); err != nil {
			return res, err
		}
	case consts.ProviderTypeAKS:
		if err := cloudv1.SetAKSClusterSpec(t.Cluster, t.AKSClusterSpec); err != nil {
			return res, err
		}
	}

	if err := cloudv1.UpdateClusterStatus(t.Cluster, func(s *cloudv1.CommonK8sClusterStatus) bool {
		if reflect.DeepEqual(t.CommonK8sClusterStatus, s) {
			return false
		}
		t.CommonK8sClusterStatus.DeepCopyInto(s)
		return true
	}); err != nil {
		return res, err
	}

	if t.IsClusterChanged() {
		if e := update(ctx, t.Local, t.Cluster, true); e != nil {
			return res, e
		}
	}
	return res, err
}

func updateDelete(ctx context.Context, t *task.Context, results []deploy.Result) (res ctrl.Result, err error) {
	var deleted bool

	// If the cluster is pending delete, then the deletion tasks are not actually performed
	if t.Cluster.Status.Phase != cloudv1.ClusterPhasePendingDelete {
		var retriableErr error
		res, err, retriableErr = deploy.MinRequeue(results)
		if err == nil && retriableErr == nil {
			deleted = true
		}
		t.Cluster.Status.Phase = cloudv1.ClusterPhaseDeleting
	}
	if deleted {
		controllerutil.RemoveFinalizer(t.Cluster, task.Finalizer)
	}

	if t.IsClusterChanged() {
		if e := update(ctx, t.Local, t.Cluster, !deleted); e != nil {
			return res, e
		}
	}
	return res, err
}

func update(ctx context.Context, cli client.Client, cluster *cloudv1.Cluster, updateStatus bool) error {
	tracing.InjectTraceContext(ctx, cluster)

	// ObjectStoreStatus cannot be empty. Required by infra-api.
	cluster.Status.ObjectStore = &cloudv1.ObjectStoreStatus{
		S3: &cloudv1.Path{Path: "unimplemented"},
	}
	util.PopulateRawExtension(&cluster.Status.FromProvider)
	clusterStatus := cluster.Status.DeepCopy()

	if err := cli.Update(ctx, cluster); err != nil {
		return err
	}
	if updateStatus {
		cluster.Status = *clusterStatus
		return cli.Status().Update(ctx, cluster)
	}

	return nil
}

func forceRequeueIfClusterIsUnsynced(ctx context.Context, cluster *cloudv1.Cluster, res ctrl.Result) ctrl.Result {
	if !res.IsZero() {
		return res
	}

	logger := log.FromContext(ctx)
	if err := cluster.FullSyncedWithDetail(); err != nil {
		logger.Info("cluster is unsynced", "err", err)
		return ctrl.Result{RequeueAfter: requeueAfterOnClusterUnsynced}
	}
	return ctrl.Result{}
}
