/*
Copyright the Velero Contributors.

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 framework

import (
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/labels"
	"k8s.io/apimachinery/pkg/runtime/schema"

	"github.com/vmware-tanzu/velero/pkg/discovery"
	"github.com/vmware-tanzu/velero/pkg/plugin/velero"
	biav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v1"
	biav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/backupitemaction/v2"
	ibav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/itemblockaction/v1"
	riav1 "github.com/vmware-tanzu/velero/pkg/plugin/velero/restoreitemaction/v1"
	riav2 "github.com/vmware-tanzu/velero/pkg/plugin/velero/restoreitemaction/v2"
	"github.com/vmware-tanzu/velero/pkg/util/collections"
)

/*
Velero has a variety of Actions that can be executed on Kubernetes resources.  The Actions (BackupItemAction, RestoreItemAction
and others) implement the Applicable interface which returns a ResourceSelector for the Action.  The ResourceSelector
can specify namespaces, resource names and labels to include or exclude.  The ResourceSelector is resolved into lists
of namespaces and resources present in the backup to be matched against.  These lists and the label selector are then used to
decide whether or not the ResolvedAction should be used for a particular resource.
*/

// ResolvedAction is an action that has had the namespaces, resources names and labels to include or exclude resolved
type ResolvedAction interface {
	// ShouldUse returns true if the resolved namespaces, resource names and labels match those passed in the parameters.
	// metadata is optional and may be nil
	ShouldUse(groupResource schema.GroupResource, namespace string, metadata metav1.Object,
		log logrus.FieldLogger) bool
}

// resolvedAction is a core struct that holds the resolved namespaces, resource names and labels
type resolvedAction struct {
	ResourceIncludesExcludes  *collections.IncludesExcludes
	NamespaceIncludesExcludes *collections.IncludesExcludes
	Selector                  labels.Selector
}

func (recv resolvedAction) ShouldUse(groupResource schema.GroupResource, namespace string, metadata metav1.Object,
	log logrus.FieldLogger) bool {
	if !recv.ResourceIncludesExcludes.ShouldInclude(groupResource.String()) {
		log.Debug("Skipping action because it does not apply to this resource")
		return false
	}

	if namespace != "" && !recv.NamespaceIncludesExcludes.ShouldInclude(namespace) {
		log.Debug("Skipping action because it does not apply to this namespace")
		return false
	}

	if namespace == "" && !recv.NamespaceIncludesExcludes.IncludeEverything() {
		log.Debug("Skipping action because resource is cluster-scoped and action only applies to specific namespaces")
		return false
	}

	if metadata != nil && !recv.Selector.Matches(labels.Set(metadata.GetLabels())) {
		log.Debug("Skipping action because label selector does not match")
		return false
	}
	return true
}

// resolveAction resolves the resources, namespaces and selector into fully-qualified versions
func resolveAction(helper discovery.Helper, action velero.Applicable) (resources *collections.IncludesExcludes,
	namespaces *collections.IncludesExcludes, selector labels.Selector, err error) {
	resourceSelector, err := action.AppliesTo()
	if err != nil {
		return nil, nil, nil, err
	}

	resources = collections.GetResourceIncludesExcludes(helper, resourceSelector.IncludedResources, resourceSelector.ExcludedResources)
	namespaces = collections.NewIncludesExcludes().Includes(resourceSelector.IncludedNamespaces...).Excludes(resourceSelector.ExcludedNamespaces...)

	selector = labels.Everything()
	if resourceSelector.LabelSelector != "" {
		if selector, err = labels.Parse(resourceSelector.LabelSelector); err != nil {
			return nil, nil, nil, err
		}
	}

	return
}

type BackupItemResolvedAction struct {
	biav1.BackupItemAction
	resolvedAction
}

func NewBackupItemActionResolver(actions []biav1.BackupItemAction) BackupItemActionResolver {
	return BackupItemActionResolver{
		actions: actions,
	}
}

type BackupItemResolvedActionV2 struct {
	biav2.BackupItemAction
	resolvedAction
}

func NewBackupItemActionResolverV2(actions []biav2.BackupItemAction) BackupItemActionResolverV2 {
	return BackupItemActionResolverV2{
		actions: actions,
	}
}

func NewRestoreItemActionResolver(actions []riav1.RestoreItemAction) RestoreItemActionResolver {
	return RestoreItemActionResolver{
		actions: actions,
	}
}

func NewRestoreItemActionResolverV2(actions []riav2.RestoreItemAction) RestoreItemActionResolverV2 {
	return RestoreItemActionResolverV2{
		actions: actions,
	}
}

func NewDeleteItemActionResolver(actions []velero.DeleteItemAction) DeleteItemActionResolver {
	return DeleteItemActionResolver{
		actions: actions,
	}
}

type ActionResolver interface {
	ResolveAction(helper discovery.Helper, action velero.Applicable, log logrus.FieldLogger) (ResolvedAction, error)
}

type BackupItemActionResolver struct {
	actions []biav1.BackupItemAction
}

func (recv BackupItemActionResolver) ResolveActions(helper discovery.Helper, log logrus.FieldLogger) ([]BackupItemResolvedAction, error) {
	var resolved []BackupItemResolvedAction
	for _, action := range recv.actions {
		resources, namespaces, selector, err := resolveAction(helper, action)
		if err != nil {
			return nil, err
		}
		res := BackupItemResolvedAction{
			BackupItemAction: action,
			resolvedAction: resolvedAction{
				ResourceIncludesExcludes:  resources,
				NamespaceIncludesExcludes: namespaces,
				Selector:                  selector,
			},
		}
		resolved = append(resolved, res)
	}
	return resolved, nil
}

type BackupItemActionResolverV2 struct {
	actions []biav2.BackupItemAction
}

func (recv BackupItemActionResolverV2) ResolveActions(helper discovery.Helper, log logrus.FieldLogger) ([]BackupItemResolvedActionV2, error) {
	var resolved []BackupItemResolvedActionV2
	for _, action := range recv.actions {
		log.Debugf("resolving BackupItemAction for: %v", action)
		resources, namespaces, selector, err := resolveAction(helper, action)
		if err != nil {
			log.WithError(errors.WithStack(err)).Debugf("resolveAction error, action: %v", action)
			return nil, err
		}
		res := BackupItemResolvedActionV2{
			BackupItemAction: action,
			resolvedAction: resolvedAction{
				ResourceIncludesExcludes:  resources,
				NamespaceIncludesExcludes: namespaces,
				Selector:                  selector,
			},
		}
		resolved = append(resolved, res)
	}
	return resolved, nil
}

type RestoreItemResolvedAction struct {
	riav1.RestoreItemAction
	resolvedAction
}

type RestoreItemResolvedActionV2 struct {
	riav2.RestoreItemAction
	resolvedAction
}

type RestoreItemActionResolver struct {
	actions []riav1.RestoreItemAction
}

func (recv RestoreItemActionResolver) ResolveActions(helper discovery.Helper, log logrus.FieldLogger) ([]RestoreItemResolvedAction, error) {
	var resolved []RestoreItemResolvedAction
	for _, action := range recv.actions {
		resources, namespaces, selector, err := resolveAction(helper, action)
		if err != nil {
			return nil, err
		}
		res := RestoreItemResolvedAction{
			RestoreItemAction: action,
			resolvedAction: resolvedAction{
				ResourceIncludesExcludes:  resources,
				NamespaceIncludesExcludes: namespaces,
				Selector:                  selector,
			},
		}
		resolved = append(resolved, res)
	}
	return resolved, nil
}

type RestoreItemActionResolverV2 struct {
	actions []riav2.RestoreItemAction
}

func (recv RestoreItemActionResolverV2) ResolveActions(helper discovery.Helper, log logrus.FieldLogger) ([]RestoreItemResolvedActionV2, error) {
	var resolved []RestoreItemResolvedActionV2
	for _, action := range recv.actions {
		resources, namespaces, selector, err := resolveAction(helper, action)
		if err != nil {
			return nil, err
		}
		res := RestoreItemResolvedActionV2{
			RestoreItemAction: action,
			resolvedAction: resolvedAction{
				ResourceIncludesExcludes:  resources,
				NamespaceIncludesExcludes: namespaces,
				Selector:                  selector,
			},
		}
		resolved = append(resolved, res)
	}
	return resolved, nil
}

type DeleteItemResolvedAction struct {
	velero.DeleteItemAction
	resolvedAction
}

type DeleteItemActionResolver struct {
	actions []velero.DeleteItemAction
}

func (recv DeleteItemActionResolver) ResolveActions(helper discovery.Helper, log logrus.FieldLogger) ([]DeleteItemResolvedAction, error) {
	var resolved []DeleteItemResolvedAction
	for _, action := range recv.actions {
		resources, namespaces, selector, err := resolveAction(helper, action)
		if err != nil {
			return nil, err
		}
		res := DeleteItemResolvedAction{
			DeleteItemAction: action,
			resolvedAction: resolvedAction{
				ResourceIncludesExcludes:  resources,
				NamespaceIncludesExcludes: namespaces,
				Selector:                  selector,
			},
		}
		resolved = append(resolved, res)
	}
	return resolved, nil
}

type ItemBlockResolvedAction struct {
	ibav1.ItemBlockAction
	resolvedAction
}

type ItemBlockActionResolver struct {
	actions []ibav1.ItemBlockAction
}

func NewItemBlockActionResolver(actions []ibav1.ItemBlockAction) ItemBlockActionResolver {
	return ItemBlockActionResolver{
		actions: actions,
	}
}

func (recv ItemBlockActionResolver) ResolveActions(helper discovery.Helper, log logrus.FieldLogger) ([]ItemBlockResolvedAction, error) {
	var resolved []ItemBlockResolvedAction
	for _, action := range recv.actions {
		resources, namespaces, selector, err := resolveAction(helper, action)
		if err != nil {
			return nil, err
		}
		res := ItemBlockResolvedAction{
			ItemBlockAction: action,
			resolvedAction: resolvedAction{
				ResourceIncludesExcludes:  resources,
				NamespaceIncludesExcludes: namespaces,
				Selector:                  selector,
			},
		}
		resolved = append(resolved, res)
	}
	return resolved, nil
}
