// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0

package hasher

import (
	"crypto/sha256"
	"encoding/json"
	"fmt"
	"sort"

	"sigs.k8s.io/kustomize/kyaml/yaml"
)

// SortArrayAndComputeHash sorts a string array and
// returns a hash for it
func SortArrayAndComputeHash(s []string) (string, error) {
	sort.Strings(s)
	data, err := json.Marshal(s)
	if err != nil {
		return "", err
	}
	return encode(hex256(string(data)))
}

// Copied from https://github.com/kubernetes/kubernetes
// /blob/master/pkg/kubectl/util/hash/hash.go
func encode(hex string) (string, error) {
	if len(hex) < 10 {
		return "", fmt.Errorf(
			"input length must be at least 10")
	}
	enc := []rune(hex[:10])
	for i := range enc {
		switch enc[i] {
		case '0':
			enc[i] = 'g'
		case '1':
			enc[i] = 'h'
		case '3':
			enc[i] = 'k'
		case 'a':
			enc[i] = 'm'
		case 'e':
			enc[i] = 't'
		}
	}
	return string(enc), nil
}

// hex256 returns the hex form of the sha256 of the argument.
func hex256(data string) string {
	return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
}

// Hasher computes the hash of an RNode.
type Hasher struct{}

// Hash returns a hash of the argument.
func (h *Hasher) Hash(node *yaml.RNode) (r string, err error) {
	var encoded string
	switch node.GetKind() {
	case "ConfigMap":
		encoded, err = encodeConfigMap(node)
	case "Secret":
		encoded, err = encodeSecret(node)
	default:
		var encodedBytes []byte
		encodedBytes, err = json.Marshal(node.YNode())
		encoded = string(encodedBytes)
	}
	if err != nil {
		return "", err
	}
	return encode(hex256(encoded))
}

func getNodeValues(
	node *yaml.RNode, paths []string) (map[string]interface{}, error) {
	values := make(map[string]interface{})
	for _, p := range paths {
		vn, err := node.Pipe(yaml.Lookup(p))
		if err != nil {
			return map[string]interface{}{}, err
		}
		if vn == nil {
			values[p] = ""
			continue
		}
		if vn.YNode().Kind != yaml.ScalarNode {
			vs, err := vn.MarshalJSON()
			if err != nil {
				return map[string]interface{}{}, err
			}
			// data, binaryData and stringData are all maps
			var v map[string]interface{}
			json.Unmarshal(vs, &v)
			values[p] = v
		} else {
			values[p] = vn.YNode().Value
		}
	}
	return values, nil
}

// encodeConfigMap encodes a ConfigMap.
// Data, Kind, and Name are taken into account.
// BinaryData is included if it's not empty to avoid useless key in output.
func encodeConfigMap(node *yaml.RNode) (string, error) {
	// get fields
	paths := []string{"metadata/name", "data", "binaryData"}
	values, err := getNodeValues(node, paths)
	if err != nil {
		return "", err
	}
	m := map[string]interface{}{
		"kind": "ConfigMap",
		"name": values["metadata/name"],
		"data": values["data"],
	}
	if _, ok := values["binaryData"].(map[string]interface{}); ok {
		m["binaryData"] = values["binaryData"]
	}

	// json.Marshal sorts the keys in a stable order in the encoding
	data, err := json.Marshal(m)
	if err != nil {
		return "", err
	}
	return string(data), nil
}

// encodeSecret encodes a Secret.
// Data, Kind, Name, and Type are taken into account.
// StringData is included if it's not empty to avoid useless key in output.
func encodeSecret(node *yaml.RNode) (string, error) {
	// get fields
	paths := []string{"type", "metadata/name", "data", "stringData"}
	values, err := getNodeValues(node, paths)
	if err != nil {
		return "", err
	}
	m := map[string]interface{}{"kind": "Secret", "type": values["type"],
		"name": values["metadata/name"], "data": values["data"]}
	if _, ok := values["stringData"].(map[string]interface{}); ok {
		m["stringData"] = values["stringData"]
	}

	// json.Marshal sorts the keys in a stable order in the encoding
	data, err := json.Marshal(m)
	if err != nil {
		return "", err
	}
	return string(data), nil
}
