/*
Copyright 2022 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 v2

import (
	"bytes"
	"encoding/json"
	"errors"
	"fmt"
	"reflect"
	"strings"
	"text/template"

	"github.com/go-openapi/jsonreference"
	"k8s.io/kubectl/pkg/util/term"
)

type explainError string

func (e explainError) Error() string {
	return string(e)
}

func WithBuiltinTemplateFuncs(tmpl *template.Template) *template.Template {
	return tmpl.Funcs(map[string]interface{}{
		"throw": func(e string, args ...any) (string, error) {
			errString := fmt.Sprintf(e, args...)
			return "", explainError(errString)
		},
		"toJson": func(obj any) (string, error) {
			res, err := json.Marshal(obj)
			return string(res), err
		},
		"toPrettyJson": func(obj any) (string, error) {
			res, err := json.MarshalIndent(obj, "", "    ")
			if err != nil {
				return "", err
			}
			return string(res), err
		},
		"fail": func(message string) (string, error) {
			return "", errors.New(message)
		},
		"wrap": func(l int, s string) (string, error) {
			buf := bytes.NewBuffer(nil)
			writer := term.NewWordWrapWriter(buf, uint(l))
			_, err := writer.Write([]byte(s))
			if err != nil {
				return "", err
			}
			return buf.String(), nil
		},
		"split": func(s string, sep string) []string {
			return strings.Split(s, sep)
		},
		"join": func(sep string, strs ...string) string {
			return strings.Join(strs, sep)
		},
		"include": func(name string, data interface{}) (string, error) {
			buf := bytes.NewBuffer(nil)
			if err := tmpl.ExecuteTemplate(buf, name, data); err != nil {
				return "", err
			}
			return buf.String(), nil
		},
		"ternary": func(a, b any, condition bool) any {
			if condition {
				return a
			}
			return b
		},
		"first": func(list any) (any, error) {
			if list == nil {
				return nil, errors.New("list is empty")
			}

			tp := reflect.TypeOf(list).Kind()
			switch tp {
			case reflect.Slice, reflect.Array:
				l2 := reflect.ValueOf(list)

				l := l2.Len()
				if l == 0 {
					return nil, errors.New("list is empty")
				}

				return l2.Index(0).Interface(), nil
			default:
				return nil, fmt.Errorf("first cannot be used on type: %T", list)
			}
		},
		"last": func(list any) (any, error) {
			if list == nil {
				return nil, errors.New("list is empty")
			}

			tp := reflect.TypeOf(list).Kind()
			switch tp {
			case reflect.Slice, reflect.Array:
				l2 := reflect.ValueOf(list)

				l := l2.Len()
				if l == 0 {
					return nil, errors.New("list is empty")
				}

				return l2.Index(l - 1).Interface(), nil
			default:
				return nil, fmt.Errorf("last cannot be used on type: %T", list)
			}
		},
		"indent": func(amount int, str string) string {
			pad := strings.Repeat(" ", amount)
			return pad + strings.Replace(str, "\n", "\n"+pad, -1)
		},
		"dict": func(keysAndValues ...any) (map[string]any, error) {
			if len(keysAndValues)%2 != 0 {
				return nil, errors.New("expected even # of arguments")
			}

			res := map[string]any{}
			for i := 0; i+1 < len(keysAndValues); i = i + 2 {
				if key, ok := keysAndValues[i].(string); ok {
					res[key] = keysAndValues[i+1]
				} else {
					return nil, fmt.Errorf("key of type %T is not a string as expected", key)
				}
			}

			return res, nil
		},
		"contains": func(list any, value any) bool {
			if list == nil {
				return false
			}

			val := reflect.ValueOf(list)
			switch val.Kind() {
			case reflect.Array:
			case reflect.Slice:
				for i := 0; i < val.Len(); i++ {
					cur := val.Index(i)
					if cur.CanInterface() && reflect.DeepEqual(cur.Interface(), value) {
						return true
					}
				}
				return false
			default:
				return false
			}
			return false
		},
		"set": func(dict map[string]any, keysAndValues ...any) (any, error) {
			if len(keysAndValues)%2 != 0 {
				return nil, errors.New("expected even number of arguments")
			}

			copyDict := make(map[string]any, len(dict))
			for k, v := range dict {
				copyDict[k] = v
			}

			for i := 0; i < len(keysAndValues); i += 2 {
				key, ok := keysAndValues[i].(string)
				if !ok {
					return nil, errors.New("keys must be strings")
				}

				copyDict[key] = keysAndValues[i+1]
			}

			return copyDict, nil
		},
		"list": func(values ...any) ([]any, error) {
			return values, nil
		},
		"add": func(value, operand int) int {
			return value + operand
		},
		"sub": func(value, operand int) int {
			return value - operand
		},
		"mul": func(value, operand int) int {
			return value * operand
		},
		"resolveRef": func(refAny any, document map[string]any) map[string]any {
			refString, ok := refAny.(string)
			if !ok {
				// if passed nil, or wrong type just treat the same
				// way as unresolved reference (makes for easier templates)
				return nil
			}

			// Resolve field path encoded by the ref
			ref, err := jsonreference.New(refString)
			if err != nil {
				// Unrecognized ref format.
				return nil
			}

			if !ref.HasFragmentOnly {
				// Downloading is not supported. Treat as not found
				return nil
			}

			fragment := ref.GetURL().Fragment
			components := strings.Split(fragment, "/")
			cur := document

			for _, k := range components {
				if len(k) == 0 {
					// first component is usually empty (#/components/) , etc
					continue
				}

				next, ok := cur[k].(map[string]any)
				if !ok {
					return nil
				}

				cur = next
			}
			return cur
		},
	})
}
