package stores

import (
	"encoding/json"
	"fmt"
	"strconv"
	"strings"
)

const mapSeparator = "__map_"
const listSeparator = "__list_"

// flattenAndMerge flattens the provided value and merges into the
// into map using prefix
func flattenAndMerge(into map[string]interface{}, prefix string, value interface{}) {
	flattenedValue := flattenValue(value)
	if flattenedValue, ok := flattenedValue.(map[string]interface{}); ok {
		for flatK, flatV := range flattenedValue {
			into[prefix+flatK] = flatV
		}
	} else {
		into[prefix] = value
	}
}

func flattenValue(value interface{}) interface{} {
	var output interface{}
	switch value := value.(type) {
	case map[string]interface{}:
		newMap := make(map[string]interface{})
		for k, v := range value {
			flattenAndMerge(newMap, mapSeparator+k, v)
		}
		output = newMap
	case []interface{}:
		newMap := make(map[string]interface{})
		for i, v := range value {
			flattenAndMerge(newMap, listSeparator+fmt.Sprintf("%d", i), v)
		}
		output = newMap
	default:
		output = value
	}
	return output
}

// Flatten flattens a map with potentially nested maps into a flat
// map. Only string keys are allowed on both the top-level map and
// child maps.
func Flatten(in map[string]interface{}) map[string]interface{} {
	newMap := make(map[string]interface{})
	for k, v := range in {
		if flat, ok := flattenValue(v).(map[string]interface{}); ok {
			for flatK, flatV := range flat {
				newMap[k+flatK] = flatV
			}
		} else {
			newMap[k] = v
		}
	}
	return newMap
}

// FlattenMetadata flattens a Metadata struct into a flat map.
func FlattenMetadata(md Metadata) (map[string]interface{}, error) {
	var mdMap map[string]interface{}
	jsonBytes, err := json.Marshal(md)
	if err != nil {
		return nil, err
	}
	err = json.Unmarshal(jsonBytes, &mdMap)
	if err != nil {
		return nil, err
	}

	flat := Flatten(mdMap)
	return flat, nil
}

type token interface{}

type mapToken struct {
	key string
}

type listToken struct {
	position int
}

// tokenize converts a path generated by Flatten to be used as a key
// in the flattened map, and converts it to a slice of tokens
func tokenize(path string) []token {
	const (
		StateNormal = 0
		StateMap    = iota
		StateList   = iota
	)
	var tokens []token
	state := StateNormal
	lastTokenEnd := 0
	i := 0
	finishPrevToken := func() {
		var t token
		switch state {
		case StateNormal:
			t = mapToken{path[lastTokenEnd:i]}
		case StateMap:
			t = mapToken{path[lastTokenEnd+len(mapSeparator) : i]}
		case StateList:
			pos, _ := strconv.Atoi(path[lastTokenEnd+len(listSeparator) : i])
			t = listToken{pos}
		}
		lastTokenEnd = i
		tokens = append(tokens, t)
	}
	for i < len(path) {
		if strings.HasPrefix(path[i:], mapSeparator) {
			finishPrevToken()
			state = StateMap
			i += len(mapSeparator)
		} else if strings.HasPrefix(path[i:], listSeparator) {
			finishPrevToken()
			state = StateList
			i += len(listSeparator)
		} else {
			i++
		}
	}
	finishPrevToken()
	return tokens
}

// unflatten takes the currentNode, currentToken, nextToken and value
// and populates currentNode such that currentToken can be considered
// processed. It inspects nextToken to decide what type to allocate
// and assign under currentNode.
func unflatten(currentNode interface{}, currentToken, nextToken token, value interface{}) interface{} {
	switch currentToken := currentToken.(type) {
	case mapToken:
		currentNode := currentNode.(map[string]interface{})
		switch nextToken := nextToken.(type) {
		case mapToken:
			if _, ok := currentNode[currentToken.key]; !ok {
				currentNode[currentToken.key] = make(map[string]interface{})
			}
			next := currentNode[currentToken.key].(map[string]interface{})
			return next
		case listToken:
			if _, ok := currentNode[currentToken.key]; !ok {
				currentNode[currentToken.key] = make([]interface{}, nextToken.position+1)
			}
			next := currentNode[currentToken.key].([]interface{})
			if nextToken.position >= len(next) {
				// Grow the slice and reassign it
				newNext := make([]interface{}, nextToken.position+1)
				copy(newNext, next)
				next = newNext
				currentNode[currentToken.key] = next
			}
			return next
		default:
			currentNode[currentToken.key] = value
		}
	case listToken:
		currentNode := currentNode.([]interface{})
		switch nextToken := nextToken.(type) {
		case mapToken:
			if currentNode[currentToken.position] == nil {
				currentNode[currentToken.position] = make(map[string]interface{})
			}
			next := currentNode[currentToken.position].(map[string]interface{})
			return next
		case listToken:
			if currentNode[currentToken.position] == nil {
				currentNode[currentToken.position] = make([]interface{}, nextToken.position+1)
			}
			next := currentNode[currentToken.position].([]interface{})
			if nextToken.position >= len(next) {
				// Grow the slice and reassign it
				newNext := make([]interface{}, nextToken.position+1)
				copy(newNext, next)
				next = newNext
				currentNode[currentToken.position] = next
			}
			return next
		default:
			currentNode[currentToken.position] = value
		}
	}
	return nil
}

// Unflatten unflattens a map flattened by Flatten
func Unflatten(in map[string]interface{}) map[string]interface{} {
	newMap := make(map[string]interface{})
	for k, v := range in {
		var current interface{} = newMap
		tokens := append(tokenize(k), nil)
		for i := 0; i < len(tokens)-1; i++ {
			current = unflatten(current, tokens[i], tokens[i+1], v)
		}
	}
	return newMap
}

// UnflattenMetadata unflattens a map flattened by FlattenMetadata into Metadata
func UnflattenMetadata(in map[string]interface{}) (Metadata, error) {
	m := Unflatten(in)
	var md Metadata
	jsonBytes, err := json.Marshal(m)
	if err != nil {
		return md, err
	}
	err = json.Unmarshal(jsonBytes, &md)
	return md, err
}

// DecodeNewLines replaces \\n with \n for all string values in the map.
// Used by config stores that do not handle multi-line values (ini, dotenv).
func DecodeNewLines(m map[string]interface{}) {
	for k, v := range m {
		if s, ok := v.(string); ok {
			m[k] = strings.Replace(s, "\\n", "\n", -1)
		}
	}
}

// EncodeNewLines replaces \n with \\n for all string values in the map.
// Used by config stores that do not handle multi-line values (ini, dotenv).
func EncodeNewLines(m map[string]interface{}) {
	for k, v := range m {
		if s, ok := v.(string); ok {
			m[k] = strings.Replace(s, "\n", "\\n", -1)
		}
	}
}

// DecodeNonStrings will look for known metadata keys that are not strings and decode to the appropriate type
func DecodeNonStrings(m map[string]interface{}) error {
	if v, ok := m["mac_only_encrypted"]; ok {
		m["mac_only_encrypted"] = false
		if v == "true" {
			m["mac_only_encrypted"] = true
		}
	}
	if v, ok := m["shamir_threshold"]; ok {
		switch val := v.(type) {
		case string:
			vInt, err := strconv.Atoi(val)
			if err != nil {
				// Older versions of SOPS stored shamir_threshold as a floating point representation
				// of the actual integer. Try to parse a floating point number and see whether it
				// can be converted without loss to an integer.
				vFloat, floatErr := strconv.ParseFloat(val, 64)
				vInt = int(vFloat)
				if floatErr != nil || float64(vInt) != vFloat {
					return fmt.Errorf("shamir_threshold is not an integer: %s", err.Error())
				}
			}
			m["shamir_threshold"] = vInt
		case int:
			m["shamir_threshold"] = val
		default:
			return fmt.Errorf("shamir_threshold is neither a string nor an integer, but %T", val)
		}
	}
	return nil
}

// EncodeNonStrings will look for known metadata keys that are not strings and will encode it to strings
func EncodeNonStrings(m map[string]interface{}) {
	if v, found := m["mac_only_encrypted"]; found {
		if vBool, ok := v.(bool); ok {
			m["mac_only_encrypted"] = "false"
			if vBool {
				m["mac_only_encrypted"] = "true"
			}
		}
	}
	if v, found := m["shamir_threshold"]; found {
		if vInt, ok := v.(int); ok {
			m["shamir_threshold"] = fmt.Sprintf("%d", vInt)
		}
		// FlattenMetadata serializes the input as JSON and then deserializes it.
		// The JSON unserializer treats every number as a float, so the above 'if'
		// never applies in that situation.
		if vFloat, ok := v.(float64); ok {
			m["shamir_threshold"] = fmt.Sprintf("%.0f", vFloat)
		}
	}
}
