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

package schema

import (
	"bytes"
	"fmt"
	"sort"
	"strconv"
)

func SerializeValueForHash(buf *bytes.Buffer, val interface{}, schema *Schema) {
	if val == nil {
		buf.WriteRune(';')
		return
	}

	switch schema.Type {
	case TypeBool:
		if val.(bool) {
			buf.WriteRune('1')
		} else {
			buf.WriteRune('0')
		}
	case TypeInt:
		buf.WriteString(strconv.Itoa(val.(int)))
	case TypeFloat:
		buf.WriteString(strconv.FormatFloat(val.(float64), 'g', -1, 64))
	case TypeString:
		buf.WriteString(val.(string))
	case TypeList:
		buf.WriteRune('(')
		l := val.([]interface{})
		for _, innerVal := range l {
			serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
		}
		buf.WriteRune(')')
	case TypeMap:

		m := val.(map[string]interface{})
		var keys []string
		for k := range m {
			keys = append(keys, k)
		}
		sort.Strings(keys)
		buf.WriteRune('[')
		for _, k := range keys {
			innerVal := m[k]
			if innerVal == nil {
				continue
			}
			buf.WriteString(k)
			buf.WriteRune(':')

			switch innerVal := innerVal.(type) {
			case int:
				buf.WriteString(strconv.Itoa(innerVal))
			case float64:
				buf.WriteString(strconv.FormatFloat(innerVal, 'g', -1, 64))
			case string:
				buf.WriteString(innerVal)
			default:
				panic(fmt.Sprintf("unknown value type in TypeMap %T", innerVal))
			}

			buf.WriteRune(';')
		}
		buf.WriteRune(']')
	case TypeSet:
		buf.WriteRune('{')
		s := val.(*Set)
		for _, innerVal := range s.List() {
			serializeCollectionMemberForHash(buf, innerVal, schema.Elem)
		}
		buf.WriteRune('}')
	default:
		panic("unknown schema type to serialize")
	}
	buf.WriteRune(';')
}

// SerializeValueForHash appends a serialization of the given resource config
// to the given buffer, guaranteeing deterministic results given the same value
// and schema.
//
// Its primary purpose is as input into a hashing function in order
// to hash complex substructures when used in sets, and so the serialization
// is not reversible.
func SerializeResourceForHash(buf *bytes.Buffer, val interface{}, resource *Resource) {
	if val == nil {
		return
	}
	sm := resource.Schema
	m := val.(map[string]interface{})
	var keys []string
	for k := range sm {
		keys = append(keys, k)
	}
	sort.Strings(keys)
	for _, k := range keys {
		innerSchema := sm[k]
		// Skip attributes that are not user-provided. Computed attributes
		// do not contribute to the hash since their ultimate value cannot
		// be known at plan/diff time.
		if !(innerSchema.Required || innerSchema.Optional) {
			continue
		}

		buf.WriteString(k)
		buf.WriteRune(':')
		innerVal := m[k]
		SerializeValueForHash(buf, innerVal, innerSchema)
	}
}

func serializeCollectionMemberForHash(buf *bytes.Buffer, val interface{}, elem interface{}) {
	switch tElem := elem.(type) {
	case *Schema:
		SerializeValueForHash(buf, val, tElem)
	case *Resource:
		buf.WriteRune('<')
		SerializeResourceForHash(buf, val, tElem)
		buf.WriteString(">;")
	default:
		panic(fmt.Sprintf("invalid element type: %T", tElem))
	}
}
