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

package schema

import (
	"fmt"
	"strings"
)

// MapFieldReader reads fields out of an untyped map[string]string to
// the best of its ability.
type MapFieldReader struct {
	Map    MapReader
	Schema map[string]*Schema
}

func (r *MapFieldReader) ReadField(address []string) (FieldReadResult, error) {
	k := strings.Join(address, ".")
	schemaList := addrToSchema(address, r.Schema)
	if len(schemaList) == 0 {
		return FieldReadResult{}, nil
	}

	schema := schemaList[len(schemaList)-1]
	switch schema.Type {
	case TypeBool, TypeInt, TypeFloat, TypeString:
		return r.readPrimitive(address, schema)
	case TypeList:
		return readListField(r, address)
	case TypeMap:
		return r.readMap(k, schema)
	case TypeSet:
		return r.readSet(address, schema)
	case typeObject:
		return readObjectField(r, address, schema.Elem.(map[string]*Schema))
	default:
		panic(fmt.Sprintf("Unknown type: %s", schema.Type))
	}
}

func (r *MapFieldReader) readMap(k string, schema *Schema) (FieldReadResult, error) {
	result := make(map[string]interface{})
	resultSet := false

	// If the name of the map field is directly in the map with an
	// empty string, it means that the map is being deleted, so mark
	// that is is set.
	if v, ok := r.Map.Access(k); ok && v == "" {
		resultSet = true
	}

	prefix := k + "."
	r.Map.Range(func(k, v string) bool {
		if strings.HasPrefix(k, prefix) {
			resultSet = true

			key := k[len(prefix):]
			if key != "%" && key != "#" {
				result[key] = v
			}
		}

		return true
	})

	err := mapValuesToPrimitive(k, result, schema)
	if err != nil {
		return FieldReadResult{}, nil //nolint:nilerr // Leave legacy flatmap handling
	}

	var resultVal interface{}
	if resultSet {
		resultVal = result
	}

	return FieldReadResult{
		Value:  resultVal,
		Exists: resultSet,
	}, nil
}

func (r *MapFieldReader) readPrimitive(
	address []string, schema *Schema) (FieldReadResult, error) {
	k := strings.Join(address, ".")
	result, ok := r.Map.Access(k)
	if !ok {
		return FieldReadResult{}, nil
	}

	returnVal, err := stringToPrimitive(result, false, schema)
	if err != nil {
		return FieldReadResult{}, err
	}

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

func (r *MapFieldReader) readSet(
	address []string, schema *Schema) (FieldReadResult, error) {
	// copy address to ensure we don't modify the argument
	address = append([]string(nil), address...)

	// Get the number of elements in the list
	countRaw, err := r.readPrimitive(
		append(address, "#"), &Schema{Type: TypeInt})
	if err != nil {
		return FieldReadResult{}, err
	}
	if !countRaw.Exists {
		// No count, means we have no list
		countRaw.Value = 0
	}

	// Create the set that will be our result
	set := schema.ZeroValue().(*Set)

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

	// Go through the map and find all the set items
	prefix := strings.Join(address, ".") + "."
	countExpected := countRaw.Value.(int)
	countActual := make(map[string]struct{})
	completed := r.Map.Range(func(k, _ string) bool {
		if !strings.HasPrefix(k, prefix) {
			return true
		}
		if strings.HasPrefix(k, prefix+"#") {
			// Ignore the count field
			return true
		}

		// Split the key, since it might be a sub-object like "idx.field"
		parts := strings.Split(k[len(prefix):], ".")
		idx := parts[0]

		var raw FieldReadResult
		raw, err = r.ReadField(append(address, idx))
		if err != nil {
			return false
		}
		if !raw.Exists {
			// This shouldn't happen because we just verified it does exist
			panic("missing field in set: " + k + "." + idx)
		}

		set.Add(raw.Value)

		// Due to the way multimap readers work, if we've seen the number
		// of fields we expect, then exit so that we don't read later values.
		// For example: the "set" map might have "ports.#", "ports.0", and
		// "ports.1", but the "state" map might have those plus "ports.2".
		// We don't want "ports.2"
		countActual[idx] = struct{}{}

		return len(countActual) < countExpected
	})
	if !completed && err != nil {
		return FieldReadResult{}, err
	}

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

// MapReader is an interface that is given to MapFieldReader for accessing
// a "map". This can be used to have alternate implementations. For a basic
// map[string]string, use BasicMapReader.
type MapReader interface {
	Access(string) (string, bool)
	Range(func(string, string) bool) bool
}

// BasicMapReader implements MapReader for a single map.
type BasicMapReader map[string]string

func (r BasicMapReader) Access(k string) (string, bool) {
	v, ok := r[k]
	return v, ok
}

func (r BasicMapReader) Range(f func(string, string) bool) bool {
	for k, v := range r {
		if cont := f(k, v); !cont {
			return false
		}
	}

	return true
}
