/*
Copyright 2019 The Kubernetes 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 fi

import (
	"fmt"
	"reflect"

	"k8s.io/klog/v2"
	"k8s.io/kops/util/pkg/reflectutils"
)

type HasDependencies[T SubContext] interface {
	GetDependencies(tasks map[string]Task[T]) []Task[T]
}

type CloudupHasDependencies = HasDependencies[CloudupSubContext]
type InstallHasDependencies = HasDependencies[InstallSubContext]
type NodeupHasDependencies = HasDependencies[NodeupSubContext]

// NotADependency is a marker type to prevent FindTaskDependencies() from considering it a potential dependency.
type NotADependency[T SubContext] struct{}

type NodeupNotADependency = NotADependency[NodeupSubContext]
type CloudupNotADependency = NotADependency[CloudupSubContext]

var _ CloudupHasDependencies = &CloudupNotADependency{}
var _ NodeupHasDependencies = &NodeupNotADependency{}

func (NotADependency[T]) GetDependencies(map[string]Task[T]) []Task[T] {
	return nil
}

// FindTaskDependencies returns a map from each task's key to the discovered list of dependencies
func FindTaskDependencies[T SubContext](tasks map[string]Task[T]) map[string][]string {
	taskToId := make(map[interface{}]string)
	for k, t := range tasks {
		taskToId[t] = k
	}

	edges := make(map[string][]string)

	for k, t := range tasks {
		task := t

		var dependencies []Task[T]
		if hd, ok := task.(HasDependencies[T]); ok {
			dependencies = hd.GetDependencies(tasks)
		} else {
			dependencies = reflectForDependencies(tasks, task)
		}

		var dependencyKeys []string
		for _, dep := range dependencies {
			// Skip nils, including interface nils
			if dep == nil || reflect.ValueOf(dep).IsNil() {
				continue
			}
			dependencyKey, found := taskToId[dep]
			if !found {
				klog.Fatalf("dependency for task %T:%q not found: %v", t, k, dep)
			}
			dependencyKeys = append(dependencyKeys, dependencyKey)
		}

		edges[k] = dependencyKeys
	}

	klog.V(4).Infof("Dependencies:")
	for k, v := range edges {
		klog.V(4).Infof("\t%s:\t%v", k, v)
	}

	return edges
}

func reflectForDependencies[T SubContext](tasks map[string]Task[T], task Task[T]) []Task[T] {
	v := reflect.ValueOf(task).Elem()
	return getDependencies(tasks, v)
}

// FindDependencies will try to infer dependencies for an arbitrary object
func FindDependencies[T SubContext](tasks map[string]Task[T], o interface{}) []Task[T] {
	if hd, ok := o.(HasDependencies[T]); ok {
		return hd.GetDependencies(tasks)
	}

	v := reflect.ValueOf(o).Elem()
	return getDependencies(tasks, v)
}

func getDependencies[T SubContext](tasks map[string]Task[T], v reflect.Value) []Task[T] {
	var dependencies []Task[T]

	visitor := func(path *reflectutils.FieldPath, f *reflect.StructField, v reflect.Value) error {
		if reflectutils.IsPrimitiveValue(v) {
			return nil
		}

		switch v.Kind() {
		case reflect.String:
			return nil

		case reflect.Interface, reflect.Ptr, reflect.Slice, reflect.Map:
			// The recursive walk will descend into this; we can ignore here
			return nil

		case reflect.Struct:
			if path.IsEmpty() {
				// Ignore self - we are a struct, but not our own dependency!
				return nil
			}

			// TODO: Can we / should we use a type-switch statement
			intf := v.Addr().Interface()
			if hd, ok := intf.(HasDependencies[T]); ok {
				deps := hd.GetDependencies(tasks)
				dependencies = append(dependencies, deps...)
				// Add the direct dependency if it's a task as well
				if dep, ok := intf.(Task[T]); ok {
					dependencies = append(dependencies, dep)
				}
			} else if dep, ok := intf.(Task[T]); ok {
				dependencies = append(dependencies, dep)
			} else if _, ok := intf.(Resource); ok {
				// Ignore: not a dependency, unless we explicitly implement HasDependencies (e.g. TaskDependentResource)
			} else {
				return fmt.Errorf("Unhandled type for %q: %T", path, v.Interface())
			}
			return reflectutils.SkipReflection

		default:
			klog.Infof("Unhandled kind for %q: %T", path, v.Interface())
			return fmt.Errorf("Unhandled kind for %q: %v", path, v.Kind())
		}
	}

	err := reflectutils.ReflectRecursive(v, visitor, &reflectutils.ReflectOptions{DeprecatedDoubleVisit: true})
	if err != nil {
		klog.Fatalf("unexpected error finding dependencies %v", err)
	}

	return dependencies
}
