// Copyright (c) The OpenTofu Authors
// SPDX-License-Identifier: MPL-2.0
// Copyright (c) 2023 HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package hcl2shim

import (
	"fmt"
	"strconv"
	"strings"

	"github.com/zclconf/go-cty/cty/convert"

	"github.com/zclconf/go-cty/cty"
)

// FlatmapValueFromHCL2 converts a value from HCL2 (really, from the cty dynamic
// types library that HCL2 uses) to a map compatible with what would be
// produced by the "flatmap" package.
//
// The type of the given value informs the structure of the resulting map.
// The value must be of an object type or this function will panic.
//
// Flatmap values can only represent maps when they are of primitive types,
// so the given value must not have any maps of complex types or the result
// is undefined.
func FlatmapValueFromHCL2(v cty.Value) map[string]string {
	if v.IsNull() {
		return nil
	}

	if !v.Type().IsObjectType() {
		panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", v.Type()))
	}

	m := make(map[string]string)
	flatmapValueFromHCL2Map(m, "", v)
	return m
}

func flatmapValueFromHCL2Value(m map[string]string, key string, val cty.Value) {
	ty := val.Type()
	switch {
	case ty.IsPrimitiveType() || ty == cty.DynamicPseudoType:
		flatmapValueFromHCL2Primitive(m, key, val)
	case ty.IsObjectType() || ty.IsMapType():
		flatmapValueFromHCL2Map(m, key+".", val)
	case ty.IsTupleType() || ty.IsListType() || ty.IsSetType():
		flatmapValueFromHCL2Seq(m, key+".", val)
	default:
		panic(fmt.Sprintf("cannot encode %s to flatmap", ty.FriendlyName()))
	}
}

func flatmapValueFromHCL2Primitive(m map[string]string, key string, val cty.Value) {
	if !val.IsKnown() {
		m[key] = UnknownVariableValue
		return
	}
	if val.IsNull() {
		// Omit entirely
		return
	}

	var err error
	val, err = convert.Convert(val, cty.String)
	if err != nil {
		// Should not be possible, since all primitive types can convert to string.
		panic(fmt.Sprintf("invalid primitive encoding to flatmap: %s", err))
	}
	m[key] = val.AsString()
}

func flatmapValueFromHCL2Map(m map[string]string, prefix string, val cty.Value) {
	if val.IsNull() {
		// Omit entirely
		return
	}
	if !val.IsKnown() {
		switch {
		case val.Type().IsObjectType():
			// Whole objects can't be unknown in flatmap, so instead we'll
			// just write all of the attribute values out as unknown.
			for name, aty := range val.Type().AttributeTypes() {
				flatmapValueFromHCL2Value(m, prefix+name, cty.UnknownVal(aty))
			}
		default:
			m[prefix+"%"] = UnknownVariableValue
		}
		return
	}

	len := 0
	for it := val.ElementIterator(); it.Next(); {
		ak, av := it.Element()
		name := ak.AsString()
		flatmapValueFromHCL2Value(m, prefix+name, av)
		len++
	}
	if !val.Type().IsObjectType() { // objects don't have an explicit count included, since their attribute count is fixed
		m[prefix+"%"] = strconv.Itoa(len)
	}
}

func flatmapValueFromHCL2Seq(m map[string]string, prefix string, val cty.Value) {
	if val.IsNull() {
		// Omit entirely
		return
	}
	if !val.IsKnown() {
		m[prefix+"#"] = UnknownVariableValue
		return
	}

	// For sets this won't actually generate exactly what helper/schema would've
	// generated, because we don't have access to the set key function it
	// would've used. However, in practice it doesn't actually matter what the
	// keys are as long as they are unique, so we'll just generate sequential
	// indexes for them as if it were a list.
	//
	// An important implication of this, however, is that the set ordering will
	// not be consistent across mutations and so different keys may be assigned
	// to the same value when round-tripping. Since this shim is intended to
	// be short-lived and not used for round-tripping, we accept this.
	i := 0
	for it := val.ElementIterator(); it.Next(); {
		_, av := it.Element()
		key := prefix + strconv.Itoa(i)
		flatmapValueFromHCL2Value(m, key, av)
		i++
	}
	m[prefix+"#"] = strconv.Itoa(i)
}

// HCL2ValueFromFlatmap converts a map compatible with what would be produced
// by the "flatmap" package to a HCL2 (really, the cty dynamic types library
// that HCL2 uses) object type.
//
// The intended result type must be provided in order to guide how the
// map contents are decoded. This must be an object type or this function
// will panic.
//
// Flatmap values can only represent maps when they are of primitive types,
// so the given type must not have any maps of complex types or the result
// is undefined.
//
// The result may contain null values if the given map does not contain keys
// for all of the different key paths implied by the given type.
func HCL2ValueFromFlatmap(m map[string]string, ty cty.Type) (cty.Value, error) {
	if m == nil {
		return cty.NullVal(ty), nil
	}
	if !ty.IsObjectType() {
		panic(fmt.Sprintf("HCL2ValueFromFlatmap called on %#v", ty))
	}

	return hcl2ValueFromFlatmapObject(m, "", ty.AttributeTypes())
}

func hcl2ValueFromFlatmapValue(m map[string]string, key string, ty cty.Type) (cty.Value, error) {
	var val cty.Value
	var err error
	switch {
	case ty.IsPrimitiveType():
		val, err = hcl2ValueFromFlatmapPrimitive(m, key, ty)
	case ty.IsObjectType():
		val, err = hcl2ValueFromFlatmapObject(m, key+".", ty.AttributeTypes())
	case ty.IsTupleType():
		val, err = hcl2ValueFromFlatmapTuple(m, key+".", ty.TupleElementTypes())
	case ty.IsMapType():
		val, err = hcl2ValueFromFlatmapMap(m, key+".", ty)
	case ty.IsListType():
		val, err = hcl2ValueFromFlatmapList(m, key+".", ty)
	case ty.IsSetType():
		val, err = hcl2ValueFromFlatmapSet(m, key+".", ty)
	default:
		err = fmt.Errorf("cannot decode %s from flatmap", ty.FriendlyName())
	}

	if err != nil {
		return cty.DynamicVal, err
	}
	return val, nil
}

func hcl2ValueFromFlatmapPrimitive(m map[string]string, key string, ty cty.Type) (cty.Value, error) {
	rawVal, exists := m[key]
	if !exists {
		return cty.NullVal(ty), nil
	}
	if rawVal == UnknownVariableValue {
		return cty.UnknownVal(ty), nil
	}

	var err error
	val := cty.StringVal(rawVal)
	val, err = convert.Convert(val, ty)
	if err != nil {
		// This should never happen for _valid_ input, but flatmap data might
		// be tampered with by the user and become invalid.
		return cty.DynamicVal, fmt.Errorf("invalid value for %q in state: %w", key, err)
	}

	return val, nil
}

func hcl2ValueFromFlatmapObject(m map[string]string, prefix string, atys map[string]cty.Type) (cty.Value, error) {
	vals := make(map[string]cty.Value)
	for name, aty := range atys {
		val, err := hcl2ValueFromFlatmapValue(m, prefix+name, aty)
		if err != nil {
			return cty.DynamicVal, err
		}
		vals[name] = val
	}
	return cty.ObjectVal(vals), nil
}

func hcl2ValueFromFlatmapTuple(m map[string]string, prefix string, etys []cty.Type) (cty.Value, error) {
	var vals []cty.Value

	// if the container is unknown, there is no count string
	listName := strings.TrimRight(prefix, ".")
	if m[listName] == UnknownVariableValue {
		return cty.UnknownVal(cty.Tuple(etys)), nil
	}

	countStr, exists := m[prefix+"#"]
	if !exists {
		return cty.NullVal(cty.Tuple(etys)), nil
	}
	if countStr == UnknownVariableValue {
		return cty.UnknownVal(cty.Tuple(etys)), nil
	}

	count, err := strconv.Atoi(countStr)
	if err != nil {
		return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %w", prefix, err)
	}
	if count != len(etys) {
		return cty.DynamicVal, fmt.Errorf("wrong number of values for %q in state: got %d, but need %d", prefix, count, len(etys))
	}

	vals = make([]cty.Value, len(etys))
	for i, ety := range etys {
		key := prefix + strconv.Itoa(i)
		val, err := hcl2ValueFromFlatmapValue(m, key, ety)
		if err != nil {
			return cty.DynamicVal, err
		}
		vals[i] = val
	}
	return cty.TupleVal(vals), nil
}

func hcl2ValueFromFlatmapMap(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
	vals := make(map[string]cty.Value)
	ety := ty.ElementType()

	// if the container is unknown, there is no count string
	listName := strings.TrimRight(prefix, ".")
	if m[listName] == UnknownVariableValue {
		return cty.UnknownVal(ty), nil
	}

	// We actually don't really care about the "count" of a map for our
	// purposes here, but we do need to check if it _exists_ in order to
	// recognize the difference between null (not set at all) and empty.
	if strCount, exists := m[prefix+"%"]; !exists {
		return cty.NullVal(ty), nil
	} else if strCount == UnknownVariableValue {
		return cty.UnknownVal(ty), nil
	}

	for fullKey := range m {
		if !strings.HasPrefix(fullKey, prefix) {
			continue
		}

		// The flatmap format doesn't allow us to distinguish between keys
		// that contain periods and nested objects, so by convention a
		// map is only ever of primitive type in flatmap, and we just assume
		// that the remainder of the raw key (dots and all) is the key we
		// want in the result value.
		key := fullKey[len(prefix):]
		if key == "%" {
			// Ignore the "count" key
			continue
		}

		val, err := hcl2ValueFromFlatmapValue(m, fullKey, ety)
		if err != nil {
			return cty.DynamicVal, err
		}
		vals[key] = val
	}

	if len(vals) == 0 {
		return cty.MapValEmpty(ety), nil
	}
	return cty.MapVal(vals), nil
}

func hcl2ValueFromFlatmapList(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
	var vals []cty.Value

	// if the container is unknown, there is no count string
	listName := strings.TrimRight(prefix, ".")
	if m[listName] == UnknownVariableValue {
		return cty.UnknownVal(ty), nil
	}

	countStr, exists := m[prefix+"#"]
	if !exists {
		return cty.NullVal(ty), nil
	}
	if countStr == UnknownVariableValue {
		return cty.UnknownVal(ty), nil
	}

	count, err := strconv.Atoi(countStr)
	if err != nil {
		return cty.DynamicVal, fmt.Errorf("invalid count value for %q in state: %w", prefix, err)
	}

	ety := ty.ElementType()
	if count == 0 {
		return cty.ListValEmpty(ety), nil
	}

	vals = make([]cty.Value, count)
	for i := 0; i < count; i++ {
		key := prefix + strconv.Itoa(i)
		val, err := hcl2ValueFromFlatmapValue(m, key, ety)
		if err != nil {
			return cty.DynamicVal, err
		}
		vals[i] = val
	}

	return cty.ListVal(vals), nil
}

func hcl2ValueFromFlatmapSet(m map[string]string, prefix string, ty cty.Type) (cty.Value, error) {
	var vals []cty.Value
	ety := ty.ElementType()

	// if the container is unknown, there is no count string
	listName := strings.TrimRight(prefix, ".")
	if m[listName] == UnknownVariableValue {
		return cty.UnknownVal(ty), nil
	}

	strCount, exists := m[prefix+"#"]
	if !exists {
		return cty.NullVal(ty), nil
	} else if strCount == UnknownVariableValue {
		return cty.UnknownVal(ty), nil
	}

	// Keep track of keys we've seen, se we don't add the same set value
	// multiple times. The cty.Set will normally de-duplicate values, but we may
	// have unknown values that would not show as equivalent.
	seen := map[string]bool{}

	for fullKey := range m {
		if !strings.HasPrefix(fullKey, prefix) {
			continue
		}
		subKey := fullKey[len(prefix):]
		if subKey == "#" {
			// Ignore the "count" key
			continue
		}
		key := fullKey
		if dot := strings.IndexByte(subKey, '.'); dot != -1 {
			key = fullKey[:dot+len(prefix)]
		}

		if seen[key] {
			continue
		}

		seen[key] = true

		// The flatmap format doesn't allow us to distinguish between keys
		// that contain periods and nested objects, so by convention a
		// map is only ever of primitive type in flatmap, and we just assume
		// that the remainder of the raw key (dots and all) is the key we
		// want in the result value.

		val, err := hcl2ValueFromFlatmapValue(m, key, ety)
		if err != nil {
			return cty.DynamicVal, err
		}
		vals = append(vals, val)
	}

	if len(vals) == 0 && strCount == "1" {
		// An empty set wouldn't be represented in the flatmap, so this must be
		// a single empty object since the count is actually 1.
		// Add an appropriately typed null value to the set.
		var val cty.Value
		switch {
		case ety.IsMapType():
			val = cty.MapValEmpty(ety)
		case ety.IsListType():
			val = cty.ListValEmpty(ety)
		case ety.IsSetType():
			val = cty.SetValEmpty(ety)
		case ety.IsObjectType():
			// TODO: cty.ObjectValEmpty
			objectMap := map[string]cty.Value{}
			for attr, ty := range ety.AttributeTypes() {
				objectMap[attr] = cty.NullVal(ty)
			}
			val = cty.ObjectVal(objectMap)
		default:
			val = cty.NullVal(ety)
		}
		vals = append(vals, val)

	} else if len(vals) == 0 {
		return cty.SetValEmpty(ety), nil
	}

	return cty.SetVal(vals), nil
}
