package approute

import (
	"context"
	"errors"
	"fmt"
	"time"

	"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"
	"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/consts"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"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/clustercli"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/constants"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/common/gcpcontainer"
	"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"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/controllers/approute/task"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appbox"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/appcontroller"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/approute"
	"github.com/tidbcloud/infra-provider/regional-server/pkg/resourcemanager/vpc"
	agentcontrolapi "github.com/tidbcloud/observability/agent-control-server/pkg/agentcontrol/api"
)

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

type APPRouteReconciler struct {
	client.Client
	Log            logr.Logger
	Scheme         *runtime.Scheme
	Config         rsconfig.RootConfig
	EventRecorder  record.EventRecorder
	appRouteRW     approute.APPRouteReaderWriter
	appBoxR        appbox.APPBoxReader
	appControllerR appcontroller.APPControllerReader
	clusterCli     clustercli.ClusterClient
	vpcR           vpc.VPCReader

	provider        string
	gcpContainerCli gcpcontainer.ClusterGetter

	AgentControlAPI agentcontrolapi.AgentControlAPI
}

func NewAPPRouteReconciler(
	client client.Client,
	log logr.Logger,
	scheme *runtime.Scheme,
	recorder record.EventRecorder,
	config rsconfig.RootConfig,
	agentControlAPI agentcontrolapi.AgentControlAPI) *APPRouteReconciler {
	r := &APPRouteReconciler{
		Client:          client,
		Log:             log,
		Scheme:          scheme,
		EventRecorder:   recorder,
		Config:          config,
		appRouteRW:      approute.NewAPPRouteReaderWriter(client),
		appBoxR:         appbox.NewAPPBoxReader(client),
		appControllerR:  appcontroller.NewAPPControllerReader(client),
		vpcR:            vpc.NewVPCReader(client),
		provider:        config.ControllerConfig.Provider,
		AgentControlAPI: agentControlAPI,
	}
	return r
}

func (r *APPRouteReconciler) WithGCPContainerClient(cli gcpcontainer.ClusterGetter) *APPRouteReconciler {
	r.gcpContainerCli = cli
	return r
}

func (r *APPRouteReconciler) WithClusterClient(cli clustercli.ClusterClient) *APPRouteReconciler {
	r.clusterCli = cli
	return r
}

func (r *APPRouteReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := r.Log.WithValues("APPRoute", req.NamespacedName)
	log.Info("Reconciling RegionalServer APProute")

	start := time.Now()
	defer func() {
		log.Info("Finish RegionalServer APPRoute reconcile", "take", time.Since(start))
	}()

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

	// var appbox v1alpha1.APPBox
	// if err := r.appBoxR.Get(ctx, client.ObjectKey{Namespace: approute.Spec.APPBoxRef.Namespace, Name: approute.Spec.APPBoxRef.Name}, &appbox); err != nil {
	// 	if apierrors.IsNotFound(err) {
	// 		log.Info("APPBox resource not found.")
	// 		return ctrl.Result{}, nil
	// 	}
	// 	return ctrl.Result{}, fmt.Errorf("unable to get APPBox resource %s: %w", approute.Spec.APPBoxRef.Name, err)
	// }
	// var vpc v1alpha1.VPC
	// vpcObjKey := client.ObjectKey{Namespace: appbox.Spec.VPCRef.Namespace, Name: appbox.Spec.VPCRef.Name}
	// if err := r.Client.Get(ctx, vpcObjKey, &vpc); err != nil {
	// 	if apierrors.IsNotFound(err) {
	// 		log.Info("VPC resource not found.")
	// 		return ctrl.Result{}, nil
	// 	}
	// 	return ctrl.Result{}, fmt.Errorf("unable to get VPC resource %s: %w", vpcObjKey.String(), err)
	// }

	c, err := (&task.Context{
		Scheme:          r.Scheme,
		Local:           r.Client,
		Logger:          log,
		APPRoute:        &approute,
		APPRouteRW:      r.appRouteRW,
		VPCR:            r.vpcR,
		Config:          r.Config,
		ClusterCli:      r.clusterCli,
		AgentControlAPI: r.AgentControlAPI,
	}).Default()
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("unable to create task context: %w", err)
	}
	switch r.provider {
	case consts.ProviderTypeGKE, string(constants.GCP):
		c.GCPContainerCli = r.gcpContainerCli
	}

	tasks := GetAPPRouteTasks(r, c)
	if approute.DeletionTimestamp.IsZero() && isNotReadyAPPRoute(&approute) {
		tasks = r.setHookOnTasks(tasks, c)

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

	var res ctrl.Result
	if approute.DeletionTimestamp.IsZero() {
		res, err = r.updateCreate(ctx, log, c, d.Deploy(ctx))
	} else {
		approute.Status.Phase = v1alpha1.APPRoutePhaseDeleting
		if err := r.appRouteRW.UpdateStatus(ctx, &approute); err != nil {
			return ctrl.Result{}, fmt.Errorf("failed to update approute status: %w", err)
		}
		res, err = r.updateDelete(ctx, log, c, d.Destroy(ctx))
	}
	if err == nil {
		r.EventRecorder.Event(&approute, corev1.EventTypeNormal, "ReconcileSuccess", "")
	} else {
		r.EventRecorder.Event(&approute, corev1.EventTypeWarning, "ReconcileFail", err.Error())
	}
	return res, err
}

func isNotReadyAPPRoute(approute *v1alpha1.APPRoute) bool {
	return approute.Status.Phase != v1alpha1.APPRoutePhaseActive &&
		approute.Status.Phase != v1alpha1.APPRoutePhaseDeleting
}

func (r *APPRouteReconciler) 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.APPRoute.Annotations[passTimeKey]
		if ok {
			return
		}
		if c.APPRoute.Annotations == nil {
			c.APPRoute.Annotations = map[string]string{}
		}
		c.APPRoute.Annotations[passTimeKey] = time.Now().Format(time.RFC3339)
		log.FromContext(ctc).Info("pass task when creating approute", "task", name)
	}
	for idx, task := range tasks {
		tasks[idx] = deploy.NewTaskWraper(task, hook, tracer)
	}
	return tasks
}

func (r *APPRouteReconciler) 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.APPRoute.Status.Phase = v1alpha1.APPRoutePhaseFailed
		t.APPRoute.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)
	}

	if e := updateAPProute(ctx, r.appRouteRW, t.APPRoute); e != nil {
		return res, fmt.Errorf("failed to update approute: %v", e)
	}
	return res, err
}

func updateAPProute(ctx context.Context, rw approute.APPRouteReaderWriter, approute *v1alpha1.APPRoute) error {
	logger := ctrl.Log.WithValues("approute", client.ObjectKeyFromObject(approute).String())
	approuteStatus := approute.Status.DeepCopy()
	if err := rw.Update(ctx, approute); err != nil {
		logger.Error(err, "failed to update approute")
		return err
	}
	approute.Status = *approuteStatus
	if err := rw.UpdateStatus(ctx, approute); err != nil {
		logger.Error(err, "failed to update approute status")
		return err
	}
	return nil
}

func (r *APPRouteReconciler) 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 encounters error",
			"retriable", retriableErr,
			"unretriable", err,
		)
	}
	if deleted {
		controllerutil.RemoveFinalizer(t.APPRoute, v1alpha1.Finalizer)
	}
	if e := r.appRouteRW.Update(ctx, t.APPRoute); e != nil {
		log.Error(e, "failed to update approute")
		return res, fmt.Errorf("failed to update approute: %v", e)
	}

	log.Info("requeue for updateDelete", "after", res.RequeueAfter.String())

	return res, err
}

func (r *APPRouteReconciler) SetupWithManager(mgr ctrl.Manager, pds ...predicate.Predicate) error {
	if r.clusterCli == nil {
		return errors.New("clusterCli is nil")
	}
	if err := mgr.GetFieldIndexer().IndexField(context.Background(), &v1alpha1.APPRoute{}, controllers.APPBoxNameField, func(o client.Object) []string {
		c := o.(*v1alpha1.APPRoute)
		var res []string
		if c.Spec.APPBoxRef != nil {
			res = append(res, c.Spec.APPBoxRef.Name)
		}
		return res
	}); err != nil {
		return err
	}

	return ctrl.NewControllerManagedBy(mgr).
		For(&v1alpha1.APPRoute{}, builder.WithPredicates(pds...)).
		WithOptions(controller.Options{RateLimiter: k8s.RetryLimiter, MaxConcurrentReconciles: 5}).
		WithEventFilter(predicate.Or(
			predicate.GenerationChangedPredicate{},
			predicate.LabelChangedPredicate{},
			predicate.AnnotationChangedPredicate{},
		)).
		Complete(r)
}
