package tidbgroupagent

import (
	"context"
	"fmt"
	"time"

	vmoperatorv1beta1 "github.com/VictoriaMetrics/operator/api/operator/v1beta1"
	"github.com/go-logr/logr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/client-go/util/workqueue"
	ctrl "sigs.k8s.io/controller-runtime"
	"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/reconcile"

	agentv2 "github.com/tidbcloud/observability/agent-control-server/api/v2"
	"github.com/tidbcloud/observability/agent-control-server/controllers/tidbgroupagent/metadata"
	"github.com/tidbcloud/observability/agent-control-server/controllers/tidbgroupagent/task"
	"github.com/tidbcloud/observability/agent-control-server/pkg/commontasks"
	commonalicloud "github.com/tidbcloud/observability/agent-control-server/pkg/commontasks/alicloud"
	commonazure "github.com/tidbcloud/observability/agent-control-server/pkg/commontasks/azure"
	"github.com/tidbcloud/observability/agent-control-server/pkg/consts"
	"github.com/tidbcloud/observability/agent-control-server/pkg/deploy"
	"github.com/tidbcloud/observability/agent-control-server/pkg/k8sclient/k8scliv2"
	"github.com/tidbcloud/observability/agent-control-server/pkg/loader"
	"github.com/tidbcloud/observability/agent-control-server/pkg/metrics"
	"github.com/tidbcloud/observability/agent-control-server/pkg/reconciler"
)

// AgentReconciler reconciles a TiDBGroupAgent object
type AgentReconciler struct {
	reconciler.Reconciler
}

// +kubebuilder:rbac:groups=agent.observability.pingcap.com,resources=tidbgroupagents,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=agent.observability.pingcap.com,resources=tidbgroupagents/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=agent.observability.pingcap.com,resources=tidbgroupagents/finalizers,verbs=update

// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// the TiDBGroupAgent object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile
func (r *AgentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	return metrics.WrapReconciledFunc(consts.AgentTidbGroup, ctx, req, r.reconcile)
}

func (r *AgentReconciler) reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	l := ctrl.Log.WithValues("agent", consts.AgentTidbGroup, "namespace", req.Namespace, "name", req.Name)

	// Fetch the TiDBGroupAgent instance.
	var agent agentv2.TiDBGroupAgent
	if err := r.Get(ctx, req.NamespacedName, &agent); err != nil {
		if apierrors.IsNotFound(err) {
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, fmt.Errorf("unable to get tidbgroupagent %s: %v", req.NamespacedName, err)
	}

	// Check if the agent is paused or in migration state.
	if agent.GetAnnotations()[consts.AnnotationKeyPauseReconcile4Migration] == "true" {
		return ctrl.Result{}, nil
	}
	if agent.GetAnnotations()[consts.AnnotationKeyPauseReconcile] == "true" && agent.DeletionTimestamp.IsZero() {
		return ctrl.Result{}, nil
	}

	// Prepare local and remote k8s clients.
	clientGetter, err := k8scliv2.NetworkClientGetter(
		r.Client,
		loader.RemoteScheme(vmoperatorv1beta1.AddToScheme),
		consts.VendorToProvider(r.Config.O11yConfig.Vendor),
		agent.Spec.KubernetesClusterName,
		r.Config,
	)
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("unable to get k8s network client getter: %v", err)
	}

	// Core task context for the reconciler.
	c := &reconciler.Context[*agentv2.TiDBGroupAgent]{
		Agent:     &agent,
		L:         l,
		Local:     r.Client,
		Remote:    k8scliv2.NewCacheClientGetter(clientGetter),
		Config:    r.Config,
		Templates: r.Templates,
		AWS:       r.AWS,
		GCP:       r.GCP,
		O11yAPI:   r.O11yAPI,
	}

	// Prepare the tasks based on the agent's status.
	var tasks []deploy.Task
	if agent.Status.Phase != agentv2.O11yAgentPhaseHibernated {
		tasks = r.buildTasks(c)
	} else {
		tasks = r.buildHibernatedTasks(c)
	}

	// Statistics of the first creation time by hook.
	if agent.DeletionTimestamp.IsZero() && (agent.Status.Phase == "" || agent.Status.Phase == agentv2.O11yAgentPhaseCreating) {
		hook := func(ctx context.Context, task deploy.Task) {
			age := time.Since(c.Agent.CreationTimestamp.Time)
			l.Info(fmt.Sprintf("pass task when creating o11y agent, task=%s, since=%v", task.Name(), age))
		}
		for i, task := range tasks {
			tasks[i] = deploy.NewTaskWrapper(task, hook)
		}
	}

	// Run tasks and update status.
	var res ctrl.Result
	d := deploy.NewDeployer(tasks, nil, l)
	if agent.DeletionTimestamp.IsZero() {
		res, err = updateStatusToNormal(ctx, l, c, d.Deploy(ctx))
	} else {
		res, err = updateStatusToDeleting(ctx, l, c, d.Destroy(ctx))
	}
	if err == nil {
		r.EventRecorder.Event(&agent, corev1.EventTypeNormal, "ReconcileSuccess", "")
	} else {
		r.EventRecorder.Event(&agent, corev1.EventTypeWarning, "ReconcileFail", err.Error())
	}
	return res, err
}

// SetupWithManager sets up the controller with the Manager.
func (r *AgentReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&agentv2.TiDBGroupAgent{}).
		Named("tidbgroupagent").
		WithOptions(controller.Options{
			MaxConcurrentReconciles: r.MaxConcurrentReconciles,
			RateLimiter:             workqueue.DefaultTypedControllerRateLimiter[reconcile.Request](),
		}).
		Complete(r)
}

func (r *AgentReconciler) buildTasks(c *reconciler.Context[*agentv2.TiDBGroupAgent]) []deploy.Task {
	clusterMeta := metadata.ClusterMeta(c.Agent)
	tasks := []deploy.Task{
		commontasks.NewGuardianTask(c.Agent, clusterMeta),
		commontasks.NewBaseBackendTask(c, clusterMeta, c.Agent.Spec.O11y, c.Agent.Spec.O11y.BizType, commontasks.DataPlaneBackendConfig()),
		task.NewVMPodScrapeTask(c),
	}
	switch r.Config.O11yConfig.Vendor {
	case consts.VendorTypeAliCloud:
		tasks = append(tasks, commonalicloud.NewPrivateLinkNameTask(c, c.Agent.Spec.KubernetesClusterName))
	case consts.VendorTypeAzure:
		tasks = append(tasks,
			commonazure.NewKubeletIdentityTask(c, clusterMeta),
			commonazure.NewPrivateLinkIPTask(c, c.Agent.Spec.KubernetesClusterName))
	}
	tasks = append(tasks,
		commontasks.NewHelmRepoTask(c, clusterMeta, r.Config.O11yConfig.CustomerConfigsV2.NextGen.Repos),
		task.NewVectorTask(c),
		commontasks.NewVectorRestartTask(c, clusterMeta, consts.AgentTidbGroup, fmt.Sprintf("tidb%s", c.Agent.Spec.SharedPoolID)),
		commontasks.NewDrainNodeTask(c, clusterMeta),
		commontasks.NewUpdateVersionTask(c.Agent, c.Config.O11yConfig.Version, clusterMeta),
	)
	if c.Agent.Annotations[consts.AnnotationKeySkipDataPlaneDelete] == "true" {
		return []deploy.Task{
			commontasks.NewGuardianTask(c.Agent, clusterMeta),
			commontasks.NewBaseBackendTask(c, clusterMeta, c.Agent.Spec.O11y, c.Agent.Spec.O11y.BizType, commontasks.DataPlaneBackendConfig()),
		}
	}
	return tasks
}

func (r *AgentReconciler) buildHibernatedTasks(c *reconciler.Context[*agentv2.TiDBGroupAgent]) []deploy.Task {
	return []deploy.Task{
		commontasks.NewGuardianTask(c.Agent, metadata.ClusterMeta(c.Agent)),
	}
}

func updateStatus(ctx context.Context, cli client.Client, agent *agentv2.TiDBGroupAgent) error {
	status := agent.Status.DeepCopy()
	if err := cli.Update(ctx, agent); err != nil {
		return err
	}
	agent.Status = *status
	return cli.Status().Update(ctx, agent)
}

func updateStatusToNormal(ctx context.Context, l logr.Logger, c *reconciler.Context[*agentv2.TiDBGroupAgent], results []deploy.Result) (ctrl.Result, error) {
	var retriableErr error
	var res ctrl.Result
	var err error

	res, err, retriableErr = deploy.MinRequeue(results)
	if deploy.HasPanic(results) {
		c.Agent.Status.Phase = agentv2.O11yAgentPhasePanic
	} else if c.Agent.Status.Phase == agentv2.O11yAgentPhasePanic {
		// Clear Panic on the first reconcile without Panic, even if Error remains.
		c.Agent.Status.Phase = agentv2.O11yAgentPhaseNormal
	} else if err == nil && retriableErr == nil {
		// Hibernated status will update to WakingUp status when create a cluster in the network
		// Hibernated network status can not update to normal
		if c.Agent.Status.Phase != agentv2.O11yAgentPhaseNormal && c.Agent.Status.Phase != agentv2.O11yAgentPhaseHibernated {
			age := time.Since(c.Agent.CreationTimestamp.Time)
			l.Info(fmt.Sprintf("mark o11y become running when creating o11y, since=%v", age))
			c.Agent.Status.Phase = agentv2.O11yAgentPhaseNormal
		}
	}
	if retriableErr != nil {
		l.Error(retriableErr, "update status to normal failed")
	}

	if updateErr := updateStatus(ctx, c.Local, c.Agent); updateErr != nil {
		return res, updateErr
	}
	return res, err
}

func updateStatusToDeleting(ctx context.Context, l logr.Logger, c *reconciler.Context[*agentv2.TiDBGroupAgent], results []deploy.Result) (ctrl.Result, error) {
	var deleted bool
	var res ctrl.Result
	var err error

	// If is pending delete, then the deletion tasks are not actually performed
	if c.Agent.Status.Phase != agentv2.O11yAgentPhasePendingDelete {
		var retriableErr error
		res, err, retriableErr = deploy.MinRequeue(results)
		if err == nil && retriableErr == nil {
			deleted = true
		}
		c.Agent.Status.Phase = agentv2.O11yAgentPhaseDeleting
		if retriableErr != nil {
			l.Error(retriableErr, "update status to deleting failed")
		}
	}
	if deleted {
		controllerutil.RemoveFinalizer(c.Agent, consts.AnnotationKeyO11yAgentFinalizer)
		l.Info("remove finalizer when agent deleted")
	}

	if updateErr := updateStatus(ctx, c.Local, c.Agent); updateErr != nil {
		return res, updateErr
	}
	return res, err
}
