/*
Copyright 2018 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 kunstruct provides unstructured from api machinery and factory for creating unstructured
package kunstruct

import (
	"encoding/json"
	"fmt"
	jsonpatch "github.com/evanphx/json-patch"
	"k8s.io/apimachinery/pkg/runtime/schema"
	"k8s.io/apimachinery/pkg/util/strategicpatch"
	"k8s.io/client-go/kubernetes/scheme"

	"k8s.io/apimachinery/pkg/labels"
	"sigs.k8s.io/kustomize/v3/pkg/types"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/apimachinery/pkg/runtime"
	"sigs.k8s.io/kustomize/v3/pkg/gvk"
	"sigs.k8s.io/kustomize/v3/pkg/ifc"
)

var _ ifc.Kunstructured = &UnstructAdapter{}

// UnstructAdapter wraps unstructured.Unstructured from
// https://github.com/kubernetes/apimachinery/blob/master/
//     pkg/apis/meta/v1/unstructured/unstructured.go
// to isolate dependence on apimachinery.
type UnstructAdapter struct {
	unstructured.Unstructured
}

// NewKunstructuredFromObject returns a new instance of Kunstructured.
func NewKunstructuredFromObject(obj runtime.Object) (ifc.Kunstructured, error) {
	// Convert obj to a byte stream, then convert that to JSON (Unstructured).
	marshaled, err := json.Marshal(obj)
	if err != nil {
		return &UnstructAdapter{}, err
	}
	var u unstructured.Unstructured
	err = u.UnmarshalJSON(marshaled)
	// creationTimestamp always 'null', remove it
	u.SetCreationTimestamp(metav1.Time{})
	return &UnstructAdapter{Unstructured: u}, err
}

// GetGvk returns the Gvk name of the object.
func (fs *UnstructAdapter) GetGvk() gvk.Gvk {
	x := fs.GroupVersionKind()
	return gvk.Gvk{
		Group:   x.Group,
		Version: x.Version,
		Kind:    x.Kind,
	}
}

// SetGvk set the Gvk of the object to the input Gvk
func (fs *UnstructAdapter) SetGvk(g gvk.Gvk) {
	fs.SetGroupVersionKind(toSchemaGvk(g))
}

// Copy provides a copy behind an interface.
func (fs *UnstructAdapter) Copy() ifc.Kunstructured {
	return &UnstructAdapter{*fs.DeepCopy()}
}

// Map returns the unstructured content map.
func (fs *UnstructAdapter) Map() map[string]interface{} {
	return fs.Object
}

// SetMap overrides the unstructured content map.
func (fs *UnstructAdapter) SetMap(m map[string]interface{}) {
	fs.Object = m
}

func (fs *UnstructAdapter) selectSubtree(path string) (map[string]interface{}, []string, bool, error) {
	sections, err := parseFields(path)
	if len(sections) == 0 || (err != nil) {
		return nil, nil, false, err
	}

	content := fs.UnstructuredContent()
	lastSectionIdx := len(sections)

	// There are multiple sections to walk
	for sectionIdx := 0; sectionIdx < lastSectionIdx; sectionIdx++ {
		idx := sections[sectionIdx].idx
		fields := sections[sectionIdx].fields

		if idx == -1 {
			// This section has no index
			return content, fields, true, nil
		}

		// This section is terminated by an indexed field.
		// Let's extract the slice first
		indexedField, found, err := unstructured.NestedFieldNoCopy(content, fields...)
		if !found || err != nil {
			return content, fields, found, err
		}
		s, ok := indexedField.([]interface{})
		if !ok {
			return content, fields, false, fmt.Errorf("%v is of the type %T, expected []interface{}", indexedField, indexedField)
		}
		if idx >= len(s) {
			return content, fields, false, fmt.Errorf("index %d is out of bounds", idx)
		}

		if sectionIdx == lastSectionIdx-1 {
			// This is the last section. Let's build a fake map
			// to let the rest of the field extraction to work.
			idxstring := fmt.Sprintf("[%v]", idx)
			newContent := map[string]interface{}{idxstring: s[idx]}
			newFields := []string{idxstring}
			return newContent, newFields, true, nil
		}

		newContent, ok := s[idx].(map[string]interface{})
		if !ok {
			// Only map are supported here
			return content, fields, false,
				fmt.Errorf("%#v is expected to be of type map[string]interface{}", s[idx])
		}
		content = newContent
	}

	// It seems to be an invalid path
	return nil, []string{}, false, nil
}

// GetFieldValue returns the value at the given fieldpath.
func (fs *UnstructAdapter) GetFieldValue(path string) (interface{}, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return nil, types.NoFieldError{Field: path}
	}

	s, found, err := unstructured.NestedFieldNoCopy(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return nil, types.NoFieldError{Field: path}
}

// GetString returns value at the given fieldpath.
func (fs *UnstructAdapter) GetString(path string) (string, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return "", types.NoFieldError{Field: path}
	}

	s, found, err := unstructured.NestedString(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return "", types.NoFieldError{Field: path}
}

// GetStringSlice returns value at the given fieldpath.
func (fs *UnstructAdapter) GetStringSlice(path string) ([]string, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return []string{}, types.NoFieldError{Field: path}
	}

	s, found, err := unstructured.NestedStringSlice(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return []string{}, types.NoFieldError{Field: path}
}

// GetBool returns value at the given fieldpath.
func (fs *UnstructAdapter) GetBool(path string) (bool, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return false, types.NoFieldError{Field: path}
	}

	s, found, err := unstructured.NestedBool(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return false, types.NoFieldError{Field: path}
}

// GetFloat64 returns value at the given fieldpath.
func (fs *UnstructAdapter) GetFloat64(path string) (float64, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return 0, err
	}

	s, found, err := unstructured.NestedFloat64(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return 0, types.NoFieldError{Field: path}
}

// GetInt64 returns value at the given fieldpath.
func (fs *UnstructAdapter) GetInt64(path string) (int64, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return 0, types.NoFieldError{Field: path}
	}

	s, found, err := unstructured.NestedInt64(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return 0, types.NoFieldError{Field: path}
}

// GetSlice returns value at the given fieldpath.
func (fs *UnstructAdapter) GetSlice(path string) ([]interface{}, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return nil, types.NoFieldError{Field: path}
	}

	s, found, err := unstructured.NestedSlice(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return nil, types.NoFieldError{Field: path}
}

// GetStringMap returns value at the given fieldpath.
func (fs *UnstructAdapter) GetStringMap(path string) (map[string]string, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return nil, types.NoFieldError{Field: path}
	}

	s, found, err := unstructured.NestedStringMap(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return nil, types.NoFieldError{Field: path}
}

// GetMap returns value at the given fieldpath.
func (fs *UnstructAdapter) GetMap(path string) (map[string]interface{}, error) {
	content, fields, found, err := fs.selectSubtree(path)
	if !found || err != nil {
		return nil, types.NoFieldError{Field: path}
	}

	s, found, err := unstructured.NestedMap(
		content, fields...)
	if found || err != nil {
		return s, err
	}
	return nil, types.NoFieldError{Field: path}
}

func (fs *UnstructAdapter) MatchesLabelSelector(selector string) (bool, error) {
	s, err := labels.Parse(selector)
	if err != nil {
		return false, err
	}
	return s.Matches(labels.Set(fs.GetLabels())), nil
}

func (fs *UnstructAdapter) MatchesAnnotationSelector(selector string) (bool, error) {
	s, err := labels.Parse(selector)
	if err != nil {
		return false, err
	}
	return s.Matches(labels.Set(fs.GetAnnotations())), nil
}

func (fs *UnstructAdapter) Patch(patch ifc.Kunstructured) error {
	versionedObj, err := scheme.Scheme.New(
		toSchemaGvk(patch.GetGvk()))
	merged := map[string]interface{}{}
	saveName := fs.GetName()
	switch {
	case runtime.IsNotRegisteredError(err):
		baseBytes, err := json.Marshal(fs.Map())
		if err != nil {
			return err
		}
		patchBytes, err := json.Marshal(patch.Map())
		if err != nil {
			return err
		}
		mergedBytes, err := jsonpatch.MergePatch(baseBytes, patchBytes)
		if err != nil {
			return err
		}
		err = json.Unmarshal(mergedBytes, &merged)
		if err != nil {
			return err
		}
	case err != nil:
		return err
	default:
		// Use Strategic-Merge-Patch to handle types w/ schema
		// TODO: Change this to use the new Merge package.
		// Store the name of the target object, because this name may have been munged.
		// Apply this name to the patched object.
		lookupPatchMeta, err := strategicpatch.NewPatchMetaFromStruct(versionedObj)
		if err != nil {
			return err
		}
		merged, err = strategicpatch.StrategicMergeMapPatchUsingLookupPatchMeta(
			fs.Map(),
			patch.Map(),
			lookupPatchMeta)
		if err != nil {
			return err
		}
	}
	fs.SetMap(merged)
	if len(fs.Map()) != 0 {
		// if the patch deletes the object
		// don't reset the name
		fs.SetName(saveName)
	}
	return nil
}

// toSchemaGvk converts to a schema.GroupVersionKind.
func toSchemaGvk(x gvk.Gvk) schema.GroupVersionKind {
	return schema.GroupVersionKind{
		Group:   x.Group,
		Version: x.Version,
		Kind:    x.Kind,
	}
}
