package dynamic

import (
	"context"
	"time"

	"github.com/porter-dev/porter/internal/templater/utils"

	"github.com/porter-dev/porter/internal/templater"
	"k8s.io/client-go/dynamic"
	di "k8s.io/client-go/dynamic/dynamicinformer"
	"k8s.io/client-go/tools/cache"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

// Object identifies a set of k8s objects, during Group-Version-Kind, and optionally
// a namespace and name to isolate a single object
type Object struct {
	Group    string
	Version  string
	Resource string

	// Optional, if resource is namespacable
	Namespace string

	// Optional, if attempting to get an object by name
	Name string
}

// TemplateReader reads any resource registered with the k8s apiserver
type TemplateReader struct {
	// The object to read from, identified by its group-version-kind
	Object *Object

	// The set of queries to execute, identified by a key and query string
	Queries []*templater.TemplateReaderQuery

	Client dynamic.Interface

	// The resource that's being queried
	gvr      schema.GroupVersionResource
	resource dynamic.ResourceInterface
}

// NewDynamicTemplateReader creates a new DynamicTemplateReader
func NewDynamicTemplateReader(client dynamic.Interface, obj *Object) templater.TemplateReader {
	r := &TemplateReader{
		Object: obj,
		Client: client,
	}

	objRes := schema.GroupVersionResource{
		Group:    r.Object.Group,
		Version:  r.Object.Version,
		Resource: r.Object.Resource,
	}

	// just case on the "core" group and unset it
	if r.Object.Group == "core" {
		objRes.Group = ""
	}

	r.gvr = objRes

	r.resource = r.Client.Resource(objRes).Namespace(r.Object.Namespace)

	return r
}

// ValuesFromTarget retrieves cluster values from the k8s apiserver
func (r *TemplateReader) ValuesFromTarget() (map[string]interface{}, error) {
	// if name is not empty, this is a get operation
	if r.Object.Name != "" {
		return r.valuesFromGet()
	}

	return r.valuesFromList()
}

// RegisterQuery adds a query to the list of queries to execute
func (r *TemplateReader) RegisterQuery(query *templater.TemplateReaderQuery) error {
	r.Queries = append(r.Queries, query)

	return nil
}

// Read returns the resulting queried data
func (r *TemplateReader) Read() (map[string]interface{}, error) {
	values, err := r.ValuesFromTarget()
	if err != nil {
		return nil, err
	}

	return utils.QueryValues(values, r.Queries)
}

// ReadStream listens for CRUD 	operations on resources and returns resulting
// queried data
func (r *TemplateReader) ReadStream(
	on templater.OnDataStream,
	stopCh <-chan struct{},
) error {
	factory := di.NewDynamicSharedInformerFactory(
		r.Client,
		10*time.Second,
	)

	informer := factory.ForResource(r.gvr).Informer()

	informer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc: func(obj interface{}) {
			pkt := make(map[string]interface{})
			pkt["kind"] = "create"

			u := obj.(*unstructured.Unstructured)

			queryObj := make(map[string]interface{})
			queryObj["items"] = []interface{}{u.Object}

			data, err := utils.QueryValues(queryObj, r.Queries)
			if err != nil {
				return
			}

			pkt["data"] = data
			on(pkt)
		},
		UpdateFunc: func(oldObj, newObj interface{}) {
			pkt := make(map[string]interface{})
			pkt["kind"] = "update"

			u := newObj.(*unstructured.Unstructured)

			queryObj := make(map[string]interface{})
			queryObj["items"] = []interface{}{u.Object}

			data, err := utils.QueryValues(queryObj, r.Queries)
			if err != nil {
				return
			}

			pkt["data"] = data
			on(pkt)
		},
		DeleteFunc: func(obj interface{}) {
			pkt := make(map[string]interface{})
			pkt["kind"] = "delete"

			u := obj.(*unstructured.Unstructured)

			queryObj := make(map[string]interface{})
			queryObj["items"] = []interface{}{u.Object}

			data, err := utils.QueryValues(queryObj, r.Queries)
			if err != nil {
				return
			}

			pkt["data"] = data
			on(pkt)
		},
	})

	go informer.Run(stopCh)

	return nil
}

func (r *TemplateReader) valuesFromList() (map[string]interface{}, error) {
	list, err := r.resource.List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		return nil, err
	}

	return list.UnstructuredContent(), nil
}

func (r *TemplateReader) valuesFromGet() (map[string]interface{}, error) {
	get, err := r.resource.Get(context.TODO(), r.Object.Name, metav1.GetOptions{})
	if err != nil {
		return nil, err
	}

	return get.Object, nil
}
