// 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 (
	"fmt"
	"strconv"
	"strings"
)

// FieldReaders are responsible for decoding fields out of data into
// the proper typed representation. ResourceData uses this to query data
// out of multiple sources: config, state, diffs, etc.
type FieldReader interface {
	ReadField([]string) (FieldReadResult, error)
}

// FieldReadResult encapsulates all the resulting data from reading
// a field.
type FieldReadResult struct {
	// Value is the actual read value. NegValue is the _negative_ value
	// or the items that should be removed (if they existed). NegValue
	// doesn't make sense for primitives but is important for any
	// container types such as maps, sets, lists.
	Value          interface{}
	ValueProcessed interface{}

	// Exists is true if the field was found in the data. False means
	// it wasn't found if there was no error.
	Exists bool

	// Computed is true if the field was found but the value
	// is computed.
	Computed bool
}

// ValueOrZero returns the value of this result or the zero value of the
// schema type, ensuring a consistent non-nil return value.
func (r *FieldReadResult) ValueOrZero(s *Schema) interface{} {
	if r.Value != nil {
		return r.Value
	}

	return s.ZeroValue()
}

// SchemasForFlatmapPath tries its best to find a sequence of schemas that
// the given dot-delimited attribute path traverses through.
func SchemasForFlatmapPath(path string, schemaMap map[string]*Schema) []*Schema {
	parts := strings.Split(path, ".")
	return addrToSchema(parts, schemaMap)
}

// addrToSchema finds the final element schema for the given address
// and the given schema. It returns all the schemas that led to the final
// schema. These are in order of the address (out to in).
func addrToSchema(addr []string, schemaMap map[string]*Schema) []*Schema {
	current := &Schema{
		Type: typeObject,
		Elem: schemaMap,
	}

	// If we aren't given an address, then the user is requesting the
	// full object, so we return the special value which is the full object.
	if len(addr) == 0 {
		return []*Schema{current}
	}

	result := make([]*Schema, 0, len(addr))
	for len(addr) > 0 {
		k := addr[0]
		addr = addr[1:]

	REPEAT:
		// We want to trim off the first "typeObject" since its not a
		// real lookup that people do. i.e. []string{"foo"} in a structure
		// isn't {typeObject, typeString}, its just a {typeString}.
		if len(result) > 0 || current.Type != typeObject {
			result = append(result, current)
		}

		switch t := current.Type; t {
		case TypeBool, TypeInt, TypeFloat, TypeString:
			if len(addr) > 0 {
				return nil
			}
		case TypeList, TypeSet:
			isIndex := len(addr) > 0 && addr[0] == "#"

			switch v := current.Elem.(type) {
			case *Resource:
				current = &Schema{
					Type: typeObject,
					Elem: v.Schema,
				}
			case *Schema:
				current = v
			case ValueType:
				current = &Schema{Type: v}
			default:
				// we may not know the Elem type and are just looking for the
				// index
				if isIndex {
					break
				}

				if len(addr) == 0 {
					// we've processed the address, so return what we've
					// collected
					return result
				}

				if len(addr) == 1 {
					if _, err := strconv.Atoi(addr[0]); err == nil {
						// we're indexing a value without a schema. This can
						// happen if the list is nested in another schema type.
						// Default to a TypeString like we do with a map
						current = &Schema{Type: TypeString}
						break
					}
				}

				return nil
			}

			// If we only have one more thing and the next thing
			// is a #, then we're accessing the index which is always
			// an int.
			if isIndex {
				current = &Schema{Type: TypeInt}
				break
			}

		case TypeMap:
			if len(addr) > 0 {
				switch v := current.Elem.(type) {
				case ValueType:
					current = &Schema{Type: v}
				case *Schema:
					current, _ = current.Elem.(*Schema)
				default:
					// maps default to string values. This is all we can have
					// if this is nested in another list or map.
					current = &Schema{Type: TypeString}
				}
			}
		case typeObject:
			// If we're already in the object, then we want to handle Sets
			// and Lists specially. Basically, their next key is the lookup
			// key (the set value or the list element). For these scenarios,
			// we just want to skip it and move to the next element if there
			// is one.
			if len(result) > 0 {
				lastType := result[len(result)-2].Type
				if lastType == TypeSet || lastType == TypeList {
					if len(addr) == 0 {
						break
					}

					k = addr[0]
					addr = addr[1:]
				}
			}

			m := current.Elem.(map[string]*Schema)
			val, ok := m[k]
			if !ok {
				return nil
			}

			current = val
			goto REPEAT
		}
	}

	return result
}

// readListField is a generic method for reading a list field out of a
// a FieldReader. It does this based on the assumption that there is a key
// "foo.#" for a list "foo" and that the indexes are "foo.0", "foo.1", etc.
// after that point.
func readListField(
	r FieldReader, addr []string, schema *Schema) (FieldReadResult, error) {
	addrPadded := make([]string, len(addr)+1)
	copy(addrPadded, addr)
	addrPadded[len(addrPadded)-1] = "#"

	// Get the number of elements in the list
	countResult, err := r.ReadField(addrPadded)
	if err != nil {
		return FieldReadResult{}, err
	}
	if !countResult.Exists {
		// No count, means we have no list
		countResult.Value = 0
	}

	// If we have an empty list, then return an empty list
	if countResult.Computed || countResult.Value.(int) == 0 {
		return FieldReadResult{
			Value:    []interface{}{},
			Exists:   countResult.Exists,
			Computed: countResult.Computed,
		}, nil
	}

	// Go through each count, and get the item value out of it
	result := make([]interface{}, countResult.Value.(int))
	for i, _ := range result {
		is := strconv.FormatInt(int64(i), 10)
		addrPadded[len(addrPadded)-1] = is
		rawResult, err := r.ReadField(addrPadded)
		if err != nil {
			return FieldReadResult{}, err
		}
		if !rawResult.Exists {
			// This should never happen, because by the time the data
			// gets to the FieldReaders, all the defaults should be set by
			// Schema.
			rawResult.Value = nil
		}

		result[i] = rawResult.Value
	}

	return FieldReadResult{
		Value:  result,
		Exists: true,
	}, nil
}

// readObjectField is a generic method for reading objects out of FieldReaders
// based on the assumption that building an address of []string{k, FIELD}
// will result in the proper field data.
func readObjectField(
	r FieldReader,
	addr []string,
	schema map[string]*Schema) (FieldReadResult, error) {
	result := make(map[string]interface{})
	exists := false
	for field, s := range schema {
		addrRead := make([]string, len(addr), len(addr)+1)
		copy(addrRead, addr)
		addrRead = append(addrRead, field)
		rawResult, err := r.ReadField(addrRead)
		if err != nil {
			return FieldReadResult{}, err
		}
		if rawResult.Exists {
			exists = true
		}

		result[field] = rawResult.ValueOrZero(s)
	}

	return FieldReadResult{
		Value:  result,
		Exists: exists,
	}, nil
}

// convert map values to the proper primitive type based on schema.Elem
func mapValuesToPrimitive(k string, m map[string]interface{}, schema *Schema) error {
	elemType, err := getValueType(k, schema)
	if err != nil {
		return err
	}

	switch elemType {
	case TypeInt, TypeFloat, TypeBool:
		for k, v := range m {
			vs, ok := v.(string)
			if !ok {
				continue
			}

			v, err := stringToPrimitive(vs, false, &Schema{Type: elemType})
			if err != nil {
				return err
			}

			m[k] = v
		}
	}
	return nil
}

func stringToPrimitive(
	value string, computed bool, schema *Schema) (interface{}, error) {
	var returnVal interface{}
	switch schema.Type {
	case TypeBool:
		if value == "" {
			returnVal = false
			break
		}
		if computed {
			break
		}

		v, err := strconv.ParseBool(value)
		if err != nil {
			return nil, err
		}

		returnVal = v
	case TypeFloat:
		if value == "" {
			returnVal = 0.0
			break
		}
		if computed {
			break
		}

		v, err := strconv.ParseFloat(value, 64)
		if err != nil {
			return nil, err
		}

		returnVal = v
	case TypeInt:
		if value == "" {
			returnVal = 0
			break
		}
		if computed {
			break
		}

		v, err := strconv.ParseInt(value, 0, 0)
		if err != nil {
			return nil, err
		}

		returnVal = int(v)
	case TypeString:
		returnVal = value
	default:
		panic(fmt.Sprintf("Unknown type: %s", schema.Type))
	}

	return returnVal, nil
}
