// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package namespace

import (
	"sigs.k8s.io/kustomize/api/filters/filtersutil"
	"sigs.k8s.io/kustomize/api/filters/fsslice"
	"sigs.k8s.io/kustomize/api/types"
	"sigs.k8s.io/kustomize/kyaml/kio"
	"sigs.k8s.io/kustomize/kyaml/resid"
	"sigs.k8s.io/kustomize/kyaml/yaml"
)

type Filter struct {
	// Namespace is the namespace to apply to the inputs
	Namespace string `yaml:"namespace,omitempty"`

	// FsSlice contains the FieldSpecs to locate the namespace field
	FsSlice types.FsSlice `json:"fieldSpecs,omitempty" yaml:"fieldSpecs,omitempty"`

	trackableSetter filtersutil.TrackableSetter
}

var _ kio.Filter = Filter{}
var _ kio.TrackableFilter = &Filter{}

// WithMutationTracker registers a callback which will be invoked each time a field is mutated
func (ns *Filter) WithMutationTracker(callback func(key, value, tag string, node *yaml.RNode)) {
	ns.trackableSetter.WithMutationTracker(callback)
}

func (ns Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
	return kio.FilterAll(yaml.FilterFunc(ns.run)).Filter(nodes)
}

// Run runs the filter on a single node rather than a slice
func (ns Filter) run(node *yaml.RNode) (*yaml.RNode, error) {
	// hacks for hardcoded types -- :(
	if err := ns.hacks(node); err != nil {
		return nil, err
	}

	// Remove the fieldspecs that are for hardcoded fields.  The fieldspecs
	// exist for backwards compatibility with other implementations
	// of this transformation.
	// This implementation of the namespace transformation
	// Does not use the fieldspecs for implementing cases which
	// require hardcoded logic.
	ns.FsSlice = ns.removeFieldSpecsForHacks(ns.FsSlice)

	// transformations based on data -- :)
	err := node.PipeE(fsslice.Filter{
		FsSlice:    ns.FsSlice,
		SetValue:   ns.trackableSetter.SetEntry("", ns.Namespace, yaml.NodeTagString),
		CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode
		CreateTag:  yaml.NodeTagString,
	})
	return node, err
}

// hacks applies the namespace transforms that are hardcoded rather
// than specified through FieldSpecs.
func (ns Filter) hacks(obj *yaml.RNode) error {
	gvk := resid.GvkFromNode(obj)
	if err := ns.metaNamespaceHack(obj, gvk); err != nil {
		return err
	}
	return ns.roleBindingHack(obj, gvk)
}

// metaNamespaceHack is a hack for implementing the namespace transform
// for the metadata.namespace field on namespace scoped resources.
// namespace scoped resources are determined by NOT being present
// in a hard-coded list of cluster-scoped resource types (by apiVersion and kind).
//
// This hack should be updated to allow individual resources to specify
// if they are cluster scoped through either an annotation on the resources,
// or through inlined OpenAPI on the resource as a YAML comment.
func (ns Filter) metaNamespaceHack(obj *yaml.RNode, gvk resid.Gvk) error {
	if gvk.IsClusterScoped() {
		return nil
	}
	f := fsslice.Filter{
		FsSlice: []types.FieldSpec{
			{Path: types.MetadataNamespacePath, CreateIfNotPresent: true},
		},
		SetValue:   ns.trackableSetter.SetEntry("", ns.Namespace, yaml.NodeTagString),
		CreateKind: yaml.ScalarNode, // Namespace is a ScalarNode
	}
	_, err := f.Filter(obj)
	return err
}

// roleBindingHack is a hack for implementing the namespace transform
// for RoleBinding and ClusterRoleBinding resource types.
// RoleBinding and ClusterRoleBinding have namespace set on
// elements of the "subjects" field if and only if the subject elements
// "name" is "default".  Otherwise the namespace is not set.
//
// Example:
//
// kind: RoleBinding
// subjects:
// - name: "default" # this will have the namespace set
//   ...
// - name: "something-else" # this will not have the namespace set
//   ...
func (ns Filter) roleBindingHack(obj *yaml.RNode, gvk resid.Gvk) error {
	if gvk.Kind != roleBindingKind && gvk.Kind != clusterRoleBindingKind {
		return nil
	}

	// Lookup the namespace field on all elements.
	// We should change the fieldspec so this isn't necessary.
	obj, err := obj.Pipe(yaml.Lookup(subjectsField))
	if err != nil || yaml.IsMissingOrNull(obj) {
		return err
	}

	// add the namespace to each "subject" with name: default
	err = obj.VisitElements(func(o *yaml.RNode) error {
		// The only case we need to force the namespace
		// if for the "service account". "default" is
		// kind of hardcoded here for right now.
		name, err := o.Pipe(
			yaml.Lookup("name"), yaml.Match("default"),
		)
		if err != nil || yaml.IsMissingOrNull(name) {
			return err
		}

		// set the namespace for the default account
		node, err := o.Pipe(
			yaml.LookupCreate(yaml.ScalarNode, "namespace"),
		)
		if err != nil {
			return err
		}

		return ns.trackableSetter.SetEntry("", ns.Namespace, yaml.NodeTagString)(node)

	})

	return err
}

// removeFieldSpecsForHacks removes from the list fieldspecs that
// have hardcoded implementations
func (ns Filter) removeFieldSpecsForHacks(fs types.FsSlice) types.FsSlice {
	var val types.FsSlice
	for i := range fs {
		// implemented by metaNamespaceHack
		if fs[i].Path == types.MetadataNamespacePath {
			continue
		}
		// implemented by roleBindingHack
		if fs[i].Kind == roleBindingKind && fs[i].Path == subjectsField {
			continue
		}
		// implemented by roleBindingHack
		if fs[i].Kind == clusterRoleBindingKind && fs[i].Path == subjectsField {
			continue
		}
		val = append(val, fs[i])
	}
	return val
}

const (
	subjectsField          = "subjects"
	roleBindingKind        = "RoleBinding"
	clusterRoleBindingKind = "ClusterRoleBinding"
)
