package apply

import (
	"context"
	"errors"
	"fmt"

	"github.com/rancher/wrangler/v2/pkg/apply/injectors"
	"github.com/rancher/wrangler/v2/pkg/kv"
	"github.com/rancher/wrangler/v2/pkg/merr"
	"github.com/rancher/wrangler/v2/pkg/objectset"
	"github.com/sirupsen/logrus"
	"k8s.io/apimachinery/pkg/api/meta"
	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/client-go/tools/cache"
)

// Indexer name added for cached types
const byHash = "wrangler.byObjectSetHash"

type patchKey struct {
	schema.GroupVersionKind
	objectset.ObjectKey
}

type desiredSet struct {
	a                        *apply
	ctx                      context.Context
	defaultNamespace         string
	listerNamespace          string
	ignorePreviousApplied    bool
	setOwnerReference        bool
	ownerReferenceController bool
	ownerReferenceBlock      bool
	strictCaching            bool
	restrictClusterScoped    bool
	pruneTypes               map[schema.GroupVersionKind]cache.SharedIndexInformer
	patchers                 map[schema.GroupVersionKind]Patcher
	reconcilers              map[schema.GroupVersionKind]Reconciler
	diffPatches              map[patchKey][][]byte
	informerFactory          InformerFactory
	remove                   bool
	noDelete                 bool
	noDeleteGVK              map[schema.GroupVersionKind]struct{}
	setID                    string
	objs                     *objectset.ObjectSet
	owner                    runtime.Object
	injectors                []injectors.ConfigInjector
	ratelimitingQps          float32
	injectorNames            []string
	errs                     []error

	createPlan bool
	plan       Plan
}

func (o *desiredSet) err(err error) error {
	o.errs = append(o.errs, err)
	return o.Err()
}

func (o desiredSet) Err() error {
	return merr.NewErrors(append(o.errs, o.objs.Err())...)
}

func (o desiredSet) DryRun(objs ...runtime.Object) (Plan, error) {
	o.objs = objectset.NewObjectSet()
	o.objs.Add(objs...)
	return o.dryRun()
}

func (o desiredSet) Apply(set *objectset.ObjectSet) error {
	if set == nil {
		set = objectset.NewObjectSet()
	}
	o.objs = set
	return o.apply()
}

func (o desiredSet) ApplyObjects(objs ...runtime.Object) error {
	os := objectset.NewObjectSet()
	os.Add(objs...)
	return o.Apply(os)
}

func (o desiredSet) WithDiffPatch(gvk schema.GroupVersionKind, namespace, name string, patch []byte) Apply {
	patches := map[patchKey][][]byte{}
	for k, v := range o.diffPatches {
		patches[k] = v
	}
	key := patchKey{
		GroupVersionKind: gvk,
		ObjectKey: objectset.ObjectKey{
			Name:      name,
			Namespace: namespace,
		},
	}
	patches[key] = append(patches[key], patch)
	o.diffPatches = patches
	return o
}

// WithGVK uses a known listing of existing gvks to modify the the prune types to allow for deletion of objects
func (o desiredSet) WithGVK(gvks ...schema.GroupVersionKind) Apply {
	pruneTypes := make(map[schema.GroupVersionKind]cache.SharedIndexInformer, len(gvks))
	for k, v := range o.pruneTypes {
		pruneTypes[k] = v
	}
	for _, gvk := range gvks {
		pruneTypes[gvk] = nil
	}
	o.pruneTypes = pruneTypes
	return o
}

func (o desiredSet) WithSetID(id string) Apply {
	o.setID = id
	return o
}

func (o desiredSet) WithOwnerKey(key string, gvk schema.GroupVersionKind) Apply {
	obj := &v1.PartialObjectMetadata{}
	obj.Namespace, obj.Name = kv.RSplit(key, "/")
	obj.SetGroupVersionKind(gvk)
	o.owner = obj
	return o
}

func (o desiredSet) WithOwner(obj runtime.Object) Apply {
	o.owner = obj
	return o
}

func (o desiredSet) WithSetOwnerReference(controller, block bool) Apply {
	o.setOwnerReference = true
	o.ownerReferenceController = controller
	o.ownerReferenceBlock = block
	return o
}

func (o desiredSet) WithInjector(injs ...injectors.ConfigInjector) Apply {
	o.injectors = append(o.injectors, injs...)
	return o
}

func (o desiredSet) WithInjectorName(injs ...string) Apply {
	o.injectorNames = append(o.injectorNames, injs...)
	return o
}

func (o desiredSet) WithCacheTypeFactory(factory InformerFactory) Apply {
	o.informerFactory = factory
	return o
}

func (o desiredSet) WithIgnorePreviousApplied() Apply {
	o.ignorePreviousApplied = true
	return o
}

func (o desiredSet) WithCacheTypes(igs ...InformerGetter) Apply {
	pruneTypes := make(map[schema.GroupVersionKind]cache.SharedIndexInformer, len(igs))
	for k, v := range o.pruneTypes {
		pruneTypes[k] = v
	}

	for _, ig := range igs {
		informer := ig.Informer()
		if err := addIndexerByHash(informer.GetIndexer()); err != nil {
			// Ignore repeatedly adding the same indexer for different types
			if !errors.Is(err, indexerAlreadyExistsErr) {
				logrus.Warnf("Problem adding hash indexer to informer [%s]: %v", ig.GroupVersionKind().Kind, err)
			}
		}
		pruneTypes[ig.GroupVersionKind()] = informer
	}

	o.pruneTypes = pruneTypes
	return o
}

// addIndexerByHash an Informer to index objects by the hash annotation value
func addIndexerByHash(indexer cache.Indexer) error {
	if _, alreadyAdded := indexer.GetIndexers()[byHash]; alreadyAdded {
		return fmt.Errorf("adding indexer %q: %w", byHash, indexerAlreadyExistsErr)
	}
	return indexer.AddIndexers(map[string]cache.IndexFunc{
		byHash: func(obj interface{}) ([]string, error) {
			metadata, err := meta.Accessor(obj)
			if err != nil {
				return nil, err
			}
			labels := metadata.GetLabels()
			if labels == nil || labels[LabelHash] == "" {
				return nil, nil
			}
			return []string{labels[LabelHash]}, nil
		},
	})
}

func (o desiredSet) WithPatcher(gvk schema.GroupVersionKind, patcher Patcher) Apply {
	patchers := map[schema.GroupVersionKind]Patcher{}
	for k, v := range o.patchers {
		patchers[k] = v
	}
	patchers[gvk] = patcher
	o.patchers = patchers
	return o
}

func (o desiredSet) WithReconciler(gvk schema.GroupVersionKind, reconciler Reconciler) Apply {
	reconcilers := map[schema.GroupVersionKind]Reconciler{}
	for k, v := range o.reconcilers {
		reconcilers[k] = v
	}
	reconcilers[gvk] = reconciler
	o.reconcilers = reconcilers
	return o
}

func (o desiredSet) WithStrictCaching() Apply {
	o.strictCaching = true
	return o
}
func (o desiredSet) WithDynamicLookup() Apply {
	o.strictCaching = false
	return o
}

func (o desiredSet) WithRestrictClusterScoped() Apply {
	o.restrictClusterScoped = true
	return o
}

func (o desiredSet) WithDefaultNamespace(ns string) Apply {
	if ns == "" {
		o.defaultNamespace = defaultNamespace
	} else {
		o.defaultNamespace = ns
	}
	return o
}

func (o desiredSet) WithListerNamespace(ns string) Apply {
	o.listerNamespace = ns
	return o
}

func (o desiredSet) WithRateLimiting(ratelimitingQps float32) Apply {
	o.ratelimitingQps = ratelimitingQps
	return o
}

func (o desiredSet) WithNoDelete() Apply {
	o.noDelete = true
	return o
}

func (o desiredSet) WithNoDeleteGVK(gvks ...schema.GroupVersionKind) Apply {
	if o.noDeleteGVK == nil {
		o.noDeleteGVK = make(map[schema.GroupVersionKind]struct{})
	}
	for _, curr := range gvks {
		o.noDeleteGVK[curr] = struct{}{}
	}
	return o
}

func (o desiredSet) WithContext(ctx context.Context) Apply {
	o.ctx = ctx
	return o
}
