/*
Copyright 2019 The Tekton 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 resources

import (
	"context"
	"encoding/json"
	"fmt"
	"strings"

	"github.com/tektoncd/triggers/pkg/apis/triggers"
	kerrors "k8s.io/apimachinery/pkg/api/errors"

	"k8s.io/client-go/dynamic"

	"go.uber.org/zap"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime/schema"
	discoveryclient "k8s.io/client-go/discovery"
)

// findAPIResource returns the APIResource definition using the discovery client c.
func findAPIResource(apiVersion, kind string, c discoveryclient.ServerResourcesInterface) (*metav1.APIResource, error) {
	resourceList, err := c.ServerResourcesForGroupVersion(apiVersion)
	if err != nil {
		return nil, fmt.Errorf("error getting kubernetes server resources for apiVersion %s: %s", apiVersion, err)
	}
	for i := range resourceList.APIResources {
		r := &resourceList.APIResources[i]
		if r.Kind != kind {
			continue
		}

		// Resolve GroupVersion from parent list to have consistent resource identifiers.
		if r.Version == "" || r.Group == "" {
			gv, err := schema.ParseGroupVersion(resourceList.GroupVersion)
			if err != nil {
				return nil, fmt.Errorf("error parsing parsing GroupVersion: %v", err)
			}
			r.Group = gv.Group
			r.Version = gv.Version
		}
		return r, nil
	}
	return nil, fmt.Errorf("error could not find resource with apiVersion %s and kind %s", apiVersion, kind)
}

// Create uses the kubeClient to create the resource defined in the
// TriggerResourceTemplate and returns any errors with this process
func Create(logger *zap.SugaredLogger, rt json.RawMessage, triggerName, eventID, elName, elNamespace string, c discoveryclient.ServerResourcesInterface, dc dynamic.Interface) error {
	// Assume the TriggerResourceTemplate is valid (it has an apiVersion and Kind)
	data := new(unstructured.Unstructured)
	if err := data.UnmarshalJSON(rt); err != nil {
		return fmt.Errorf("couldn't unmarshal json from the TriggerTemplate: %v", err)
	}

	data, err := addLabels(data, map[string]string{
		triggers.EventListenerLabelKey: elName,
		triggers.EventIDLabelKey:       eventID,
		triggers.TriggerLabelKey:       triggerName,
	})
	if err != nil {
		return err
	}

	namespace := data.GetNamespace()
	// Default the resource creation to the EventListenerNamespace if not found in the resource template
	if namespace == "" {
		namespace = elNamespace
	}

	// Resolve resource kind to the underlying API Resource type.
	apiResource, err := findAPIResource(data.GetAPIVersion(), data.GetKind(), c)
	if err != nil {
		return fmt.Errorf("couldn't find API resource for json: %v", err)
	}

	name := data.GetName()
	if name == "" {
		name = data.GetGenerateName()
	}
	logger.Infof("Generating resource: kind: %s, name: %s", apiResource, name)

	gvr := schema.GroupVersionResource{
		Group:    apiResource.Group,
		Version:  apiResource.Version,
		Resource: apiResource.Name,
	}

	logger.Infof("For event ID %q creating resource %v", eventID, gvr)

	if _, err := dc.Resource(gvr).Namespace(namespace).Create(context.Background(), data, metav1.CreateOptions{}); err != nil {
		if kerrors.IsUnauthorized(err) || kerrors.IsForbidden(err) {
			return err
		}
		return fmt.Errorf("couldn't create resource with group version kind %q: %v", gvr, err)
	}
	return nil
}

// addLabels adds autogenerated Tekton labels to created resources.
func addLabels(us *unstructured.Unstructured, labelsToAdd map[string]string) (*unstructured.Unstructured, error) {
	labels, _, err := unstructured.NestedStringMap(us.Object, "metadata", "labels")
	if err != nil {
		return nil, err
	}

	if labels == nil {
		labels = make(map[string]string)
	}
	for k, v := range labelsToAdd {
		l := fmt.Sprintf("%s/%s", triggers.GroupName, strings.TrimLeft(k, "/"))
		labels[l] = v
	}

	us.SetLabels(labels)
	return us, nil
}
