/*
Copyright 2021.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package network

import (
	"context"
	"fmt"
	"sync"
	"time"

	"cloud.google.com/go/pubsub"
	ec2v1alpha1 "github.com/crossplane-contrib/provider-aws/apis/ec2/v1alpha1"
	"github.com/crossplane-contrib/provider-aws/apis/eks/v1beta1"
	"github.com/go-logr/logr"
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
	"go.uber.org/multierr"
	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/client-go/tools/record"
	"sigs.k8s.io/aws-iam-authenticator/pkg/token"
	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/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"

	"github.com/tidbcloud/eks-provider/api/v1alpha1"
	"github.com/tidbcloud/eks-provider/controllers"
	"github.com/tidbcloud/eks-provider/controllers/network/task"
	taskeks "github.com/tidbcloud/eks-provider/controllers/network/task/eks"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-provider/pkg/account"
	accountsched "github.com/tidbcloud/infra-provider/pkg/account/schedule"
	"github.com/tidbcloud/infra-provider/pkg/cloud/aws"
	"github.com/tidbcloud/infra-provider/pkg/cloud/gcp"
	"github.com/tidbcloud/infra-provider/pkg/config"
	"github.com/tidbcloud/infra-provider/pkg/deploy"
	"github.com/tidbcloud/infra-provider/pkg/k8s"
	"github.com/tidbcloud/infra-provider/pkg/metrics"
	metricsctrl "github.com/tidbcloud/infra-provider/pkg/metrics/controller"
	"github.com/tidbcloud/infra-provider/pkg/namer"
	"github.com/tidbcloud/infra-provider/pkg/o11y/tracing"
	"github.com/tidbcloud/infra-provider/pkg/scheme"
	"github.com/tidbcloud/infra-provider/pkg/taskevents"
	tracerutil "github.com/tidbcloud/infra-provider/pkg/tracer"
)

var (
	networkSetupWithManagerOnce sync.Once
	tracer                      = otel.Tracer("NetworkReconciler")
)

// NetworkReconciler reconciles a Network object
type NetworkReconciler struct {
	Logger logr.Logger

	EventRecorder record.EventRecorder
	client.Client
	TokenGen token.Generator
	Cache    client.Reader
	Scheme   *runtime.Scheme
	Config   config.RootConfig
	Provider string

	AcctInfoGetter account.LegacyInfoGetter
	AcctScheduler  accountsched.Scheduler
	Manager        *k8s.Manager

	AWS      aws.Client
	GCP      gcp.Client
	TopicCli *pubsub.Topic
}

//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=networks,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=networks/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=cloud.pingcap.com,resources=networks/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.
func (r *NetworkReconciler) Reconcile(ctx context.Context, req ctrl.Request) (res ctrl.Result, err error) {
	logger := log.FromContext(ctx)

	var network cloudv1.Network
	logger.Info("Reconciling Network")
	start := time.Now()
	defer func() {
		metricsctrl.ReconcileDuration.
			WithLabelValues("network").Observe(time.Since(start).Seconds())

		info := []interface{}{"take", time.Since(start)}
		if err == nil {
			info = append(info, "result", res)
		}
		logger.Info("Finish reconcile", info...)
	}()

	namespacedName := req.NamespacedName.String()
	if err = r.Cache.Get(ctx, req.NamespacedName, &network); err != nil {
		if apierrors.IsNotFound(err) {
			r.Manager.Deregister(ctx, namespacedName)
			return ctrl.Result{}, nil
		}
		return ctrl.Result{}, fmt.Errorf("unable to get network %s: %v", req.NamespacedName, err)
	}

	// As we don't want to run network-controller in dryrun/readonly mode, so we don't need to pass an AssumeRole like cluster-controller.
	kubeCluster := k8s.NetworkKubeCluster{Network: &network}
	if network.Spec.Paused != nil && *network.Spec.Paused {
		logger.Info("Skipping paused network", "network", req.NamespacedName)

		// If a network is paused and the infra provider restarts, we need to re-register the network.
		// Otherwise, the node protector will not work when we do k8s upgrade.
		if network.Status.Phase == cloudv1.NetworkPhaseNormal {
			if err := r.Manager.Register(ctx, kubeCluster); err != nil {
				return ctrl.Result{}, fmt.Errorf("unable to register network %s: %v", req.NamespacedName, err)
			}
		}
		return ctrl.Result{}, nil
	}

	providerType := network.Spec.Provider
	if providerType != r.Provider {
		logger.Info("Mismatched provider", "expect", r.Provider, "got", providerType)
		return ctrl.Result{}, nil
	}

	c, err := (&task.Context{
		Local:          k8s.NewMutClientWithOwner(r.Client, network.DeepCopy(), namer.NewNamer()).Client(),
		Config:         r.Config,
		Provider:       &r.Config,
		Network:        &network,
		AcctInfoGetter: r.AcctInfoGetter,
		Logger:         r.Logger,
		IsMigrate:      v1alpha1.IsMigrate(&network),
	}).Default()
	if err != nil {
		return ctrl.Result{}, fmt.Errorf("unable to get task context: %v", err)
	}

	// We only register the network after finished Creating, but
	// Some task when Creating still need the client, so build a client directly
	getter, err := r.Manager.ClientGetter(ctx, namespacedName)
	if err != nil {
		logger.Info("manager client not ready", "network", req.NamespacedName, "error", err)
		g, err := k8s.NetworkClientGetter(ctx, c.Local, &network, scheme.Scheme)
		if err == nil {
			c.Remote = g
		}
	} else {
		c.Remote = getter
	}

	var tasks []deploy.Task
	if network.Status.Phase == cloudv1.NetworkPhaseHibernated {
		tasks = GetHibernatedNetworkTasks(r, c)
		if len(tasks) == 0 {
			logger.Error(fmt.Errorf("provider %s not introduce hibernated status", network.Spec.Provider), "get hibernated network task")
			return ctrl.Result{Requeue: false}, nil
		}
		r.Manager.Deregister(ctx, namespacedName)
	} else if network.Status.Phase == cloudv1.NetworkPhaseHibernating {
		tasks = GetHibernatingNetworkTasks(r, c)
		if len(tasks) == 0 {
			logger.Error(fmt.Errorf("provider %s not introduce hibernating status", network.Spec.Provider), "get hibernating network task")
			return ctrl.Result{Requeue: false}, nil
		}
	} else {
		tasks = GetNetworkTasks(r, c)
	}

	if network.DeletionTimestamp.IsZero() &&
		(network.Status.Phase == "" || network.Status.Phase == cloudv1.NetworkPhaseCreating) {
		hook := func(ctx context.Context, task deploy.Task) {

			name := task.Name()
			key := "first-pass-when-creating.cloud.pingcap.com/" + name
			_, ok := c.Network.Annotations[key]
			if ok {
				return
			}

			if c.Network.Annotations == nil {
				c.Network.Annotations = make(map[string]string)
			}
			age := time.Since(c.Network.CreationTimestamp.Time)
			c.Network.Annotations[key] = age.String()
			log.FromContext(ctx).Info("pass task when creating network", "task", name, "sinceCreate", age)
			metrics.NetworkFirstPassStageSeconds.WithLabelValues(name).Observe(age.Seconds())

			if taskevents.IsEnabled(r.Config) {
				taskevents.PublishTaskEvent(ctx, r.TopicCli, taskevents.TaskEvent{
					Resource:    "network",
					Namespace:   network.Namespace,
					Name:        network.Name,
					Task:        name,
					Status:      "done",
					ResourceAge: age.Seconds(),
					EventTime:   time.Now(),
				})
			}
		}

		var span trace.Span
		ctx, span = tracer.Start(tracing.ExtractTraceContext(ctx, &network), "Network Reconcile")
		span.SetAttributes(
			attribute.String("tenant_id", network.Labels[cloudv1.TenantTagKey]),
			attribute.String("project_id", network.Labels[cloudv1.ProjectTagKey]),
			attribute.String("namespace", network.Namespace),
			attribute.String("network_name", network.Name),
		)
		defer func() { tracerutil.EndSpan(span, err) }()

		for i, t := range tasks {
			tasks[i] = deploy.NewTaskWraper(t, hook, tracer)
		}
	}

	d := deploy.NewDeployer(tasks, r.Config.Network.IgnoreDelete, logger)

	// if network deletion timestamp is not empty, will destroy all resources. when task completed, finallizer will removed.
	// if network status is hibernating, will destroy resources except VPC and Bucket tasks. when task completed, status will update to hibernated.
	if network.DeletionTimestamp.IsZero() && network.Status.Phase != cloudv1.NetworkPhaseHibernating {
		res, err = updateCreate(ctx, c, d.Deploy(ctx))
	} else {
		res, err = updateDelete(ctx, c, d.Destroy(ctx))
	}

	if network.Status.Phase == cloudv1.NetworkPhaseNormal {
		err2 := r.Manager.Register(ctx, kubeCluster)
		if err2 != nil {
			err = multierr.Append(err, err2)
		}
	} else if len(network.Finalizers) == 0 {
		r.Manager.Deregister(ctx, namespacedName)
	}

	if err == nil {
		r.EventRecorder.Event(&network, corev1.EventTypeNormal, "ReconcileSuccess", "")
	} else {
		r.EventRecorder.Event(&network, corev1.EventTypeWarning, "ReconcileFail", err.Error())
	}

	return res, err
}

// SetupWithManager sets up the controller with the Manager.
func (r *NetworkReconciler) SetupWithManager(mgr ctrl.Manager, pds ...predicate.Predicate) error {
	networkSetupWithManagerOnce.Do(func() {
		// Index the vpcID field for VPC endpoints, so that we can list them by vpcID.
		if err := mgr.GetFieldIndexer().IndexField(context.TODO(), &ec2v1alpha1.VPCEndpoint{}, "spec.forProvider.vpcId", func(obj client.Object) []string {
			var res []string
			vpcID := obj.(*ec2v1alpha1.VPCEndpoint).Spec.ForProvider.VPCID
			if vpcID != nil {
				res = append(res, *vpcID)
			}
			return res
		}); err != nil {
			return
		}
	})

	return ctrl.NewControllerManagedBy(mgr).
		// Uncomment the following line adding a pointer to an instance of the controlled resource as an argument
		For(&cloudv1.Network{}, builder.WithPredicates(pds...)).
		Watches(
			&v1beta1.Cluster{},
			taskeks.HandlerFuncs,
		).
		WithOptions(controller.Options{RateLimiter: k8s.RetryLimiter, MaxConcurrentReconciles: controllers.GetMaxConcurrentReconciles(r.Config, "network")}).
		Complete(r)
}
