/* 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"
	"reflect"
	"time"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/tools/record"
	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/handler"
	"sigs.k8s.io/controller-runtime/pkg/log"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/source"

	"github.com/go-logr/logr"
	"github.com/pingcap/tidb-operator/pkg/apis/pingcap/v1alpha1"
	cloudv1 "github.com/tidbcloud/infra-api/api/v1"
	"github.com/tidbcloud/infra-api/pkg/providers/aws"
	"github.com/tidbcloud/shared-provider/pkg/manager"
	"github.com/tidbcloud/shared-provider/pkg/scheme"
	"github.com/tidbcloud/shared-provider/pkg/types"
	"github.com/tidbcloud/shared-provider/pkg/utils"
	"github.com/tidbcloud/shared-provider/pkg/utils/labels"
	"github.com/tidbcloud/shared-provider/pkg/utils/template"
)

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

	Client      client.Client
	InfraClient client.Client

	Cache      client.Reader
	InfraCache client.Reader

	Recorder      record.EventRecorder
	InfraRecorder record.EventRecorder

	AwsClient aws.Client

	KubeClusterName string
	Env             string // environment name, "dev", "staging", "prod", etc.

	OIDCProviderARN string

	AddonDir string

	AddonConfigMapNamespace string
	AddonConfigMapName      string

	// for aws
	AWSAccountID  string
	ImportRoleARN string

	// for gcp
	GCPProjectID string // TODO(csuzhangxc): set this value when implementing network controller for GCP

	tmpl    template.Interface
	tracker Tracker
}

// +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
// +kubebuilder:rbac:groups=core,resources=namespaces,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=serviceaccounts,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=*
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=*
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete

// 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 *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	logger := log.FromContext(ctx)
	logger.Info("Start reconcile")

	startTime := time.Now()
	defer func() {
		logger.Info("Finish reconcile",
			"time", time.Since(startTime),
		)
	}()

	net := &cloudv1.Network{}
	if err := r.InfraCache.Get(ctx, req.NamespacedName, net); err != nil {
		if apierrors.IsNotFound(err) {
			return ctrl.Result{}, nil
		}

		return ctrl.Result{}, fmt.Errorf("can't get network %s from cache: %w", req.NamespacedName, err)
	}

	curResourceVersion := net.ResourceVersion

	defer func() {
		logger = logger.WithValues("resourceVersion.before", curResourceVersion).
			WithValues("resourceVersion.after", net.ResourceVersion)
	}()

	if net.Spec.Provider != types.ProviderTypeGCPSharedTier && net.Spec.Provider != types.ProviderTypeAWSSharedTier {
		return ctrl.Result{}, nil
	}

	if kubeCluster := labels.GetKubeClusterFromLabels(net.Labels); kubeCluster != r.KubeClusterName {
		logger.Info("unexpected network scheduling", "kubecluster", kubeCluster)
		return ctrl.Result{}, nil
	}

	rescheduled, err := r.Reschedule(ctx, net)
	if err != nil {
		return ctrl.Result{}, err
	}

	if rescheduled {
		return ctrl.Result{}, nil
	}

	if err := r.sync(ctx, net); err != nil {
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

func (r *Reconciler) sync(ctx context.Context, net *cloudv1.Network) error {
	if err := r.initStatus(ctx, net); err != nil {
		return err
	}

	if err := r.syncFinalizer(ctx, net); err != nil {
		return err
	}

	// Wait for namespace deletion to trigger resync
	if net.DeletionTimestamp != nil {
		return nil
	}

	if err := r.syncNamespace(ctx, net); err != nil {
		return err
	}

	if err := r.syncDefaultSA(ctx, net); err != nil {
		return err
	}

	if err := r.syncTiDBOperator(ctx, net); err != nil {
		return err
	}

	clusters, err := NetworkClusters(ctx, r.InfraCache, net)
	if err != nil {
		return err
	}
	if len(clusters) != 0 {
		if err := r.ensureImportResource(ctx, net); err != nil {
			return err
		}
	} else {
		if err := r.cleanUpImportResource(ctx, net); err != nil {
			return err
		}
	}

	if err := r.ensureBackupBucket(ctx, net); err != nil {
		return err
	}

	if err := r.ensureBackupRole(ctx, net); err != nil {
		return err
	}

	if err := r.finalizeStatus(ctx, net); err != nil {
		return err
	}

	return nil
}

// SetupWithManager sets up the controller with the Manager.
func (r *Reconciler) SetupWithManager(mgr manager.Manager) error {
	r.Client = mgr.GetClient()
	r.InfraClient = mgr.InfraCluster().GetClient()
	r.Cache = mgr.GetCache()
	r.InfraCache = mgr.InfraCluster().GetCache()
	r.Recorder = mgr.GetEventRecorderFor("Network")
	r.InfraRecorder = mgr.InfraCluster().GetEventRecorderFor("Network")

	r.tracker = NewTracker()
	r.tracker.Add(&TypeAndKey{
		Type: reflect.TypeOf(&v1alpha1.Backup{}),
	})
	r.tmpl = template.New(scheme.Scheme)

	c, err := controller.New("Network", mgr, controller.Options{
		Reconciler: r,
	})
	if err != nil {
		return fmt.Errorf("can't new a controller: %w", err)
	}

	r.Logger = c.GetLogger()

	if err := r.initTiDBOperatorTemplate(); err != nil {
		return fmt.Errorf("can't init addon template: %w", err)
	}

	return r.watches(c, mgr)
}

func (r *Reconciler) watches(c controller.Controller, mgr manager.Manager) error {
	ptp := utils.GetPredicateForProviderTypes(types.ProviderTypeGCPSharedTier, types.ProviderTypeAWSSharedTier)
	p, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
		MatchLabels: map[string]string{
			labels.LabelKeyKubernetesCluster: r.KubeClusterName,
		},
	})
	if err != nil {
		return fmt.Errorf("invalid label selector for network: %w", err)
	}

	// watch network
	if err := c.Watch(
		source.NewKindWithCache(&cloudv1.Network{}, mgr.InfraCluster().GetCache()),
		&handler.EnqueueRequestForObject{},
		ptp,
		p,
	); err != nil {
		return fmt.Errorf("can't watch network: %w", err)
	}

	// watch cluster
	if err := c.Watch(
		source.NewKindWithCache(&cloudv1.Cluster{}, mgr.InfraCluster().GetCache()),
		&handler.Funcs{
			CreateFunc: r.handleCreateClusterEvent,
			UpdateFunc: r.handleUpdateClusterEvent,
			DeleteFunc: r.handleDeleteClusterEvent,
		},
		ptp,
		p,
	); err != nil {
		return fmt.Errorf("can't watch cluster: %w", err)
	}

	cp, err := predicate.LabelSelectorPredicate(metav1.LabelSelector{
		MatchLabels: map[string]string{
			labels.LabelKeyKubernetesCluster: r.KubeClusterName,
		},
	})
	if err != nil {
		return fmt.Errorf("invalid label selector: %w", err)
	}

	// watch namespace
	if err := c.Watch(
		source.NewKindWithCache(&corev1.Namespace{}, mgr.GetCache()),
		&handler.Funcs{
			CreateFunc: r.handleCreateNamespaceEvent,
			UpdateFunc: r.handleUpdateNamespaceEvent,
			DeleteFunc: r.handleDeleteNamespaceEvent,
		},
		cp,
	); err != nil {
		return fmt.Errorf("can't watch namespace: %w", err)
	}

	// watch addon template configmap
	if err := c.Watch(
		source.NewKindWithCache(&corev1.ConfigMap{}, mgr.GetCache()),
		&handler.Funcs{
			CreateFunc: r.handleCreateConfigMapEvent,
			UpdateFunc: r.handleUpdateConfigMapEvent,
		},
	); err != nil {
		return fmt.Errorf("can't watch configmap: %w", err)
	}

	// watch backup
	if err := c.Watch(
		source.NewKindWithCache(&v1alpha1.Backup{}, mgr.GetCache()),
		&handler.Funcs{
			CreateFunc: r.handleCreateInNamespaceResource,
			DeleteFunc: r.handleDeleteInNamespaceResource,
		},
		// v1alpha1.Backup should inherit tags from cloudv1.Backup
		// TODO: inherit tags.tidbcloud.com/kubernetes in backup controller
		// cp,
	); err != nil {
		return fmt.Errorf("can't watch backup: %w", err)
	}

	// TODO: try to watch other resources?

	return nil
}

func (r *Reconciler) ensureImportResource(
	ctx context.Context,
	network *cloudv1.Network,
) error {
	switch network.Spec.Provider {
	case types.ProviderTypeAWSSharedTier:
		return r.ensureAWSImportResource(ctx, network)
	case types.ProviderTypeGCPSharedTier:
		return r.ensureGCPImportResource(ctx, network)
	default:
		logger := log.FromContext(ctx)
		logger.Error(nil, "unknown provider type", "type", network.Spec.Provider)
		return nil
	}
}

func (r *Reconciler) cleanUpImportResource(
	ctx context.Context,
	network *cloudv1.Network,
) error {
	switch network.Spec.Provider {
	case types.ProviderTypeAWSSharedTier:
		return r.cleanUpAWSImportResource(ctx, network)
	case types.ProviderTypeGCPSharedTier:
		return r.cleanUpGCPImportResource(ctx, network)
	default:
		logger := log.FromContext(ctx)
		logger.Error(nil, "unknown provider type", "type", network.Spec.Provider)
		return nil
	}
}

// providerType: e.g. aws-shared-tier or gcp-shared-tier
// cluster: e.g. dev-us-east1-s01
// ns: infra api's namespace
func getAWSBackupRoleName(providerType, cluster, ns string) string {
	return fmt.Sprintf("%s-%s-%s-backup", providerType, cluster, ns)
}
