/*
Copyright 2022 The Karmada Authors.

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 federatedresourcequota

import (
	"context"
	"encoding/json"
	"reflect"
	"sort"

	corev1 "k8s.io/api/core/v1"
	apierrors "k8s.io/apimachinery/pkg/api/errors"
	"k8s.io/apimachinery/pkg/api/meta"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/types"
	"k8s.io/client-go/tools/record"
	"k8s.io/client-go/util/retry"
	"k8s.io/klog/v2"
	controllerruntime "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/event"
	"sigs.k8s.io/controller-runtime/pkg/handler"
	"sigs.k8s.io/controller-runtime/pkg/predicate"
	"sigs.k8s.io/controller-runtime/pkg/reconcile"

	policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1"
	workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
	"github.com/karmada-io/karmada/pkg/events"
	"github.com/karmada-io/karmada/pkg/util"
	"github.com/karmada-io/karmada/pkg/util/helper"
	"github.com/karmada-io/karmada/pkg/util/names"
)

const (
	// StatusControllerName is the controller name that will be used when reporting events and metrics.
	StatusControllerName = "federated-resource-quota-status-controller"
)

// StatusController is to collect status from work to FederatedResourceQuota.
type StatusController struct {
	client.Client // used to operate Work resources.
	EventRecorder record.EventRecorder
}

// Reconcile performs a full reconciliation for the object referred to by the Request.
// The SyncController will requeue the Request to be processed again if an error is non-nil or
// Result.Requeue is true, otherwise upon completion it will remove the work from the queue.
func (c *StatusController) Reconcile(ctx context.Context, req controllerruntime.Request) (controllerruntime.Result, error) {
	klog.V(4).Infof("FederatedResourceQuota status controller reconciling %s", req.NamespacedName.String())

	quota := &policyv1alpha1.FederatedResourceQuota{}
	if err := c.Get(ctx, req.NamespacedName, quota); err != nil {
		// The resource may no longer exist, in which case we stop processing.
		if apierrors.IsNotFound(err) {
			return controllerruntime.Result{}, nil
		}
		return controllerruntime.Result{}, err
	}

	if !quota.DeletionTimestamp.IsZero() {
		return controllerruntime.Result{}, nil
	}

	if err := c.collectQuotaStatus(ctx, quota); err != nil {
		klog.Errorf("Failed to collect status from works to federatedResourceQuota(%s), error: %v", req.NamespacedName.String(), err)
		c.EventRecorder.Eventf(quota, corev1.EventTypeWarning, events.EventReasonCollectFederatedResourceQuotaStatusFailed, err.Error())
		return controllerruntime.Result{}, err
	}
	c.EventRecorder.Eventf(quota, corev1.EventTypeNormal, events.EventReasonCollectFederatedResourceQuotaStatusSucceed, "Collect status of FederatedResourceQuota(%s) succeed.", req.NamespacedName.String())
	return controllerruntime.Result{}, nil
}

// SetupWithManager creates a controller and register to controller manager.
func (c *StatusController) SetupWithManager(mgr controllerruntime.Manager) error {
	fn := handler.MapFunc(
		func(_ context.Context, obj client.Object) []reconcile.Request {
			var requests []reconcile.Request

			quotaNamespace, namespaceExist := obj.GetLabels()[util.FederatedResourceQuotaNamespaceLabel]
			quotaName, nameExist := obj.GetLabels()[util.FederatedResourceQuotaNameLabel]
			if namespaceExist && nameExist {
				requests = append(requests, reconcile.Request{
					NamespacedName: types.NamespacedName{
						Namespace: quotaNamespace,
						Name:      quotaName,
					},
				})
			}

			return requests
		},
	)

	workPredicate := builder.WithPredicates(predicate.Funcs{
		CreateFunc: func(event.CreateEvent) bool {
			return false
		},
		UpdateFunc: func(e event.UpdateEvent) bool {
			objOld := e.ObjectOld.(*workv1alpha1.Work)
			objNew := e.ObjectNew.(*workv1alpha1.Work)
			_, namespaceExist := objNew.GetLabels()[util.FederatedResourceQuotaNamespaceLabel]
			_, nameExist := objNew.GetLabels()[util.FederatedResourceQuotaNameLabel]
			if !namespaceExist || !nameExist {
				return false
			}
			return !reflect.DeepEqual(objOld.Status, objNew.Status)
		},
		DeleteFunc: func(event.DeleteEvent) bool {
			return false
		},
		GenericFunc: func(event.GenericEvent) bool {
			return false
		},
	})
	return controllerruntime.NewControllerManagedBy(mgr).
		Named(StatusControllerName).
		For(&policyv1alpha1.FederatedResourceQuota{}).
		Watches(&workv1alpha1.Work{}, handler.EnqueueRequestsFromMapFunc(fn), workPredicate).
		Complete(c)
}

func (c *StatusController) collectQuotaStatus(ctx context.Context, quota *policyv1alpha1.FederatedResourceQuota) error {
	workList, err := helper.GetWorksByLabelsSet(ctx, c.Client, labels.Set{
		util.FederatedResourceQuotaNamespaceLabel: quota.Namespace,
		util.FederatedResourceQuotaNameLabel:      quota.Name,
	})
	if err != nil {
		klog.Errorf("Failed to list workList created by federatedResourceQuota(%s), error: %v", klog.KObj(quota).String(), err)
		return err
	}

	aggregatedStatuses, err := aggregatedStatusFormWorks(workList.Items)
	if err != nil {
		return err
	}

	quotaStatus := quota.Status.DeepCopy()
	quotaStatus.Overall = quota.Spec.Overall
	quotaStatus.AggregatedStatus = aggregatedStatuses
	quotaStatus.OverallUsed = calculateUsed(aggregatedStatuses)

	if reflect.DeepEqual(quota.Status, *quotaStatus) {
		klog.V(4).Infof("New quotaStatus are equal with old federatedResourceQuota(%s) status, no update required.", klog.KObj(quota).String())
		return nil
	}

	return retry.RetryOnConflict(retry.DefaultRetry, func() error {
		_, err = helper.UpdateStatus(ctx, c.Client, quota, func() error {
			quota.Status = *quotaStatus
			return nil
		})
		return err
	})
}

func aggregatedStatusFormWorks(works []workv1alpha1.Work) ([]policyv1alpha1.ClusterQuotaStatus, error) {
	var aggregatedStatuses []policyv1alpha1.ClusterQuotaStatus
	for index := range works {
		work := works[index]
		var applied bool
		if cond := meta.FindStatusCondition(work.Status.Conditions, workv1alpha1.WorkApplied); cond != nil {
			switch cond.Status {
			case metav1.ConditionTrue:
				applied = true
			case metav1.ConditionUnknown:
				fallthrough
			case metav1.ConditionFalse:
				applied = false
			default: // should not happen unless the condition api changed.
				panic("unexpected status")
			}
		}
		if !applied {
			klog.Warningf("Work(%s) applied failed, skip aggregated status", klog.KObj(&work).String())
			continue
		}

		if len(work.Status.ManifestStatuses) == 0 {
			klog.Warningf("The ManifestStatuses length of work(%s) is zero", klog.KObj(&work).String())
			continue
		}

		clusterName, err := names.GetClusterName(work.Namespace)
		if err != nil {
			klog.Errorf("Failed to get clusterName from work namespace %s. Error: %v.", work.Namespace, err)
			return nil, err
		}

		status := &corev1.ResourceQuotaStatus{}
		if err := json.Unmarshal(work.Status.ManifestStatuses[0].Status.Raw, status); err != nil {
			klog.Errorf("Failed to unmarshal work(%s) status to ResourceQuotaStatus", klog.KObj(&work).String())
			return nil, err
		}

		aggregatedStatus := policyv1alpha1.ClusterQuotaStatus{
			ClusterName:         clusterName,
			ResourceQuotaStatus: *status,
		}
		aggregatedStatuses = append(aggregatedStatuses, aggregatedStatus)
	}

	sort.Slice(aggregatedStatuses, func(i, j int) bool {
		return aggregatedStatuses[i].ClusterName < aggregatedStatuses[j].ClusterName
	})
	return aggregatedStatuses, nil
}

func calculateUsed(aggregatedStatuses []policyv1alpha1.ClusterQuotaStatus) corev1.ResourceList {
	overallUsed := corev1.ResourceList{}
	for index := range aggregatedStatuses {
		used := aggregatedStatuses[index].Used
		for resourceName, quantity := range used {
			r, exist := overallUsed[resourceName]
			if !exist {
				overallUsed[resourceName] = quantity
			} else {
				r.Add(quantity)
				overallUsed[resourceName] = r
			}
		}
	}
	return overallUsed
}
