package o11yroute

import (
	"context"
	"fmt"
	"time"

	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	ctrl "sigs.k8s.io/controller-runtime"
	"sigs.k8s.io/controller-runtime/pkg/builder"
	"sigs.k8s.io/controller-runtime/pkg/client"
	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	"github.com/go-logr/logr"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/o11y/tracing"
	tracerutil "github.com/tidbcloud/infra-provider/pkg/tracer"
	"github.com/tidbcloud/infra-provider/regional-server/api/v1alpha1"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/o11ycli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/utils"
	rsconfig "github.com/tidbcloud/infra-provider/regional-server/pkg/config"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/o11yroute/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/o11yroute"
)

var (
	tracer = otel.Tracer("O11yRouteReconciler")
)

type O11yRouteReconciler struct {
	client.Client
	Log           logr.Logger
	Scheme        *runtime.Scheme
	Config        rsconfig.RootConfig
	EventRecorder record.EventRecorder
	o11yRouteRW   o11yroute.O11yRouteReaderWriter

	provider string
}

func NewO11yRouteReconciler(
	client client.Client,
	log logr.Logger,
	scheme *runtime.Scheme,
	recorder record.EventRecorder,
	config rsconfig.RootConfig) *O11yRouteReconciler {
	r := &O11yRouteReconciler{
		Client:        client,
		Log:           log,
		Scheme:        scheme,
		EventRecorder: recorder,
		Config:        config,
		o11yRouteRW:   o11yroute.NewO11yRouteReaderWriter(client),
		provider:      config.ControllerConfig.Provider,
	}
	return r
}

func (r *O11yRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := r.Log.WithValues("O11yRoute", req.NamespacedName)
	logger.Info("Reconciling RegionalServer O11yRoute")
	ctx = log.IntoContext(ctx, logger)

	var err error
	var o11yroute v1alpha1.O11yRoute
	if err := r.o11yRouteRW.Get(ctx, req.NamespacedName, &o11yroute); err != nil {
		if apierrors.IsNotFound(err) {
			logger.Info("O11yRoute resource not found.")
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, fmt.Errorf("unable to get O11yRoute resource %s: %w", req.NamespacedName, err)
	}
	// do nothing if the cr is paused
	if utils.IsPaused(o11yroute.GetObjectMeta()) {
		logger.Info("reconciliation is paused through the pause annotation",
			"annotation", v1alpha1.AnnotationKeyReconciliationPaused,
			"o11yroute", req.NamespacedName.String(),
		)
		return ctrl.Result{}, nil
	}

	var vpc v1alpha1.VPC
	vpcObjKey := client.ObjectKey{Namespace: o11yroute.Spec.VPCRef.Namespace, Name: o11yroute.Spec.VPCRef.Name}
	if err := r.Get(ctx, vpcObjKey, &vpc); err != nil {
		return ctrl.Result{}, err
	}

	var appbox v1alpha1.APPBox
	appboxObjKey := client.ObjectKey{Namespace: o11yroute.Spec.APPBoxRef.Namespace, Name: o11yroute.Spec.APPBoxRef.Name}
	if err := r.Get(ctx, appboxObjKey, &appbox); err != nil {
		return ctrl.Result{}, err
	}

	tenantID, projectID := "", ""
	tenants, projects := utils.GetRelatedTenantProjects(o11yroute.GetLabels())
	if len(tenants) > 0 {
		tenantID = tenants[0]
	}
	if len(projects) > 0 {
		projectID = projects[0]
	}
	o11yEndpoint := r.Config.O11yRegionalServiceEndpoint
	if r.Config.O11yRegionalServerTestConfig.Enable &&
		r.Config.O11yRegionalServerTestConfig.IsInList(tenantID, projectID) {
		o11yEndpoint = r.Config.O11yRegionalServerTestConfig.TestEndpoint
	}

	c := (&task.Context{
		Scheme:     r.Scheme,
		Local:      r.Client,
		APPBox:     appbox.DeepCopy(),
		VPC:        vpc.DeepCopy(),
		Logger:     logger,
		O11yRoute:  &o11yroute,
		Config:     r.Config,
		O11yClient: o11ycli.NewRegionalClient(o11yEndpoint),
	})

	if o11yroute.Spec.APPRouteRef != nil {
		var appRoute v1alpha1.APPRoute
		appRouteKey := client.ObjectKey{Namespace: o11yroute.Spec.APPRouteRef.Namespace, Name: o11yroute.Spec.APPRouteRef.Name}
		if err := r.Get(ctx, appRouteKey, &appRoute); err != nil {
			if client.IgnoreNotFound(err) != nil {
				return ctrl.Result{}, err
			}
			// if not found, assume that approute is deleted, do nothing here.
		} else {
			c.APPRoute = appRoute.DeepCopy()
		}
	}

	tasks := GetO11yRouteTasks(r, c)
	if o11yroute.DeletionTimestamp.IsZero() && isNotReadyO11yRoute(&o11yroute) {
		tasks = r.setHookOnTasks(tasks, c)

		// TODO: refactor tracing
		var span trace.Span
		ctx, span = tracer.Start(tracing.ExtractTraceContext(ctx, &o11yroute), "O11yRoute Reconcile")
		span.SetAttributes(
			attribute.String("o11yroute", o11yroute.Name),
			attribute.String("namespace", o11yroute.Namespace),
			attribute.String("appbox", o11yroute.Spec.APPBoxRef.Name),
		)
		defer func() { tracerutil.EndSpan(span, err) }()
	}
	d := deploy.NewDeployer(tasks, nil, logger)

	var res ctrl.Result
	if o11yroute.DeletionTimestamp.IsZero() {
		res, err = r.updateCreate(ctx, logger, c, d.Deploy(ctx))
	} else {
		res, err = r.updateDelete(ctx, logger, c, d.Destroy(ctx))
	}
	if err == nil {
		r.EventRecorder.Event(&o11yroute, corev1.EventTypeNormal, "ReconcileSuccess", "")
	} else {
		r.EventRecorder.Event(&o11yroute, corev1.EventTypeWarning, "ReconcileFail", err.Error())
	}
	return res, err
}

func isNotReadyO11yRoute(o11yroute *v1alpha1.O11yRoute) bool {
	return o11yroute.Status.Phase != v1alpha1.O11yRoutePhaseActive &&
		o11yroute.Status.Phase != v1alpha1.O11yRoutePhaseDeleting
}

func (r *O11yRouteReconciler) setHookOnTasks(tasks []deploy.Task, c *task.Context) []deploy.Task {
	hook := func(ctc context.Context, task deploy.Task) {
		name := task.Name()
		passTimeKey := utils.GenTaskFirstPastAnnotationKey(name)
		_, ok := c.O11yRoute.Annotations[passTimeKey]
		if ok {
			return
		}
		if c.O11yRoute.Annotations == nil {
			c.O11yRoute.Annotations = map[string]string{}
		}
		c.O11yRoute.Annotations[passTimeKey] = time.Now().Format(time.RFC3339)
		log.FromContext(ctc).Info("pass task when creating o11yroute", "task", name)
	}
	for idx, task := range tasks {
		tasks[idx] = deploy.NewTaskWraper(task, hook, tracer)
	}
	return tasks
}

func (r *O11yRouteReconciler) updateCreate(ctx context.Context, log logr.Logger, t *task.Context, results []deploy.Result) (ctrl.Result, error) {
	res, err, retriableErrs := deploy.MinRequeue(results)

	if err != nil {
		log.Info("deploy create encounters error", "unretriable", err)
		t.O11yRoute.Status.Phase = v1alpha1.O11yRoutePhaseFailed
		t.O11yRoute.Status.Reason = err.Error()
	} else if retriableErrs != nil {
		// for retriable error, just log, no need to change status
		log.Info("deploy create encounters error", "retriable", retriableErrs)
	} else {
		if t.O11yRoute.Spec.Suspend {
			t.O11yRoute.Status.Phase = v1alpha1.O11yRoutePhaseSuspended
			t.O11yRoute.Status.Reason = "Cluster Suspended"
		} else {
			log.Info("mark o11yroute active when creating")
			t.O11yRoute.Status.Phase = v1alpha1.O11yRoutePhaseActive
			t.O11yRoute.Status.Reason = "Create O11y Succeed"
		}
	}
	if e := updateO11yRoute(ctx, r.o11yRouteRW, t.O11yRoute); e != nil {
		return res, fmt.Errorf("failed to update o11yroute: %v", e)
	}
	return res, err
}

func updateO11yRoute(ctx context.Context, rw o11yroute.O11yRouteReaderWriter, o11yroute *v1alpha1.O11yRoute) error {
	logger := log.FromContext(ctx)
	o11yrouteStatus := o11yroute.Status.DeepCopy()
	if err := rw.Update(ctx, o11yroute); err != nil {
		logger.Error(err, "failed to update o11yroute")
		return err
	}
	o11yroute.Status = *o11yrouteStatus
	if err := rw.UpdateStatus(ctx, o11yroute); err != nil {
		logger.Error(err, "failed to update o11yroute status")
		return err
	}
	return nil
}

func (r *O11yRouteReconciler) updateDelete(ctx context.Context, log logr.Logger, t *task.Context, results []deploy.Result) (ctrl.Result, error) {
	var deleted bool
	res, err, retriableErr := deploy.MinRequeue(results)
	if err == nil && retriableErr == nil {
		deleted = true
	} else {
		log.Info("deploy delete enconters error",
			"retriable", retriableErr,
			"unretriable", err,
		)
	}
	t.O11yRoute.Status.Phase = v1alpha1.O11yRoutePhaseDeleting

	if deleted {
		controllerutil.RemoveFinalizer(t.O11yRoute, v1alpha1.Finalizer)
	}
	if e := updateO11yRoute(ctx, r.o11yRouteRW, t.O11yRoute); e != nil {
		return res, fmt.Errorf("failed to update o11yroute: %v", e)
	}

	return res, err
}

func (r *O11yRouteReconciler) SetupWithManager(mgr ctrl.Manager, pds ...predicate.Predicate) error {
	if err := mgr.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.O11yRoute{}, controllers.APPBoxNameField, func(o client.Object) []string {
		c := o.(*v1alpha1.O11yRoute)
		res := make([]string, 0, 1)
		res = append(res, c.Spec.APPBoxRef.Name)
		return res
	}); err != nil {
		return err
	}

	return ctrl.NewControllerManagedBy(mgr).
		For(&v1alpha1.O11yRoute{}, builder.WithPredicates(pds...)).
		Complete(r)
}
