package hasher

import (
	"bytes"
	"crypto/sha256"
	"fmt"
	"sort"

	jsoniter "github.com/json-iterator/go"
	"github.com/speps/go-hashids"
	v1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	k8sapijson "sigs.k8s.io/kustomize/pseudo/k8s/apimachinery/pkg/runtime/serializer/json"
)

var hd *hashids.HashID

const (
	// ObjectHash is the hash of the object processed by Rafay
	ObjectHash = "rafay.dev/object-hash"
)

func init() {
	hd, _ = hashids.NewWithData(&hashids.HashIDData{
		Alphabet:  "abcdefghijklmnopqrstuvwxyz1234567890",
		Salt:      "***REMOVED***",
		MinLength: 7,
	})
}

// IDFromString returns new RafayID for hash id string
func IDFromString(hashID string) (int64, error) {
	ids, err := hd.DecodeInt64WithError(hashID)
	if err != nil {
		return -1, err
	}

	if len(ids) < 1 {
		return -1, fmt.Errorf("no ids could be constructed from string %s", hashID)
	}
	return ids[0], nil
}

// HashFromInt64 returns new RafayID for hash id string
func HashFromInt64(id int64) (string, error) {
	stringHash, err := hd.EncodeInt64([]int64{id})
	if err != nil {
		return "", err
	}
	return stringHash, nil
}

// HashFromHex returns new Hash for uuid string
func HashFromHex(id string) (string, error) {
	stringHash, err := hd.EncodeHex(id)
	if err != nil {
		return "", err
	}
	return stringHash, nil
}

// HashFromHex returns new Hash for uuid string
func IDFromHash(hash string) (string, error) {
	id, err := hd.DecodeHex(hash)
	if err != nil {
		return "", err
	}
	return id, nil
}

var json = k8sapijson.CaseSensitiveJsonIterator()

// GetHash returns the hash of spec/data for a kubernetes style object
func GetHash(o interface{}) (string, error) {
	var b []byte
	var err error
	b, err = json.Marshal(o)
	if err != nil {
		return "", err
	}

	bb := new(bytes.Buffer)

	iter := jsoniter.ParseBytes(json, b)
	for field := iter.ReadObject(); field != ""; field = iter.ReadObject() {
		switch field {
		case "data", "binaryData", "spec":
			bb.Write(iter.SkipAndReturnBytes())
		default:
			iter.Skip()
		}
	}

	h := sha256.New()
	h.Write(bb.Bytes())

	return fmt.Sprintf("%x", h.Sum(nil)), nil

}

//Add adds object hash to rutime object
func Add(o metav1.Object) error {

	hash, err := GetHash(o)
	if err != nil {
		return err
	}

	annotations := o.GetAnnotations()
	if annotations == nil {
		annotations = make(map[string]string)
	}
	annotations[ObjectHash] = hash
	o.SetAnnotations(annotations)

	return nil
}

// Node Hash should take Labels into the hash calculation since it can be
// set from either side: From core or from cluster
func GetNodeHashFrom(labels map[string]string, taints []v1.Taint, unscheduleable bool) (string, error) {
	//add sorted labels
	labelsKeys := make([]string, 0)
	for k := range labels {
		labelsKeys = append(labelsKeys, k)
	}
	sort.Strings(labelsKeys)
	finalLabelsAsString := ""
	for _, k := range labelsKeys {
		finalLabelsAsString += fmt.Sprintf("%s:%s,", k, labels[k])
	}
	//add sorted taints
	taintKeys := make([]string, 0)
	taintMap := make(map[string]v1.Taint)
	for _, taint := range taints {
		taintKeys = append(taintKeys, taint.Key)
		taintMap[taint.Key] = taint
	}
	sort.Strings(taintKeys)
	finalTaintsAsString := ""
	for _, k := range taintKeys {
		finalTaintsAsString += fmt.Sprintf("%s:%s:%s,", k, taintMap[k].Value, taintMap[k].Effect)
	}
	finalHashString := fmt.Sprintf("labels:%s,taints:%s,unschedulable:%v", finalLabelsAsString, finalTaintsAsString, unscheduleable)
	h := sha256.New()
	h.Write([]byte(finalHashString))
	return fmt.Sprintf("%x", h.Sum(nil)), nil
}
