package jobspec2

import (
	"fmt"
	"math"
	"math/big"
	"reflect"

	"github.com/hashicorp/hcl/v2"
	"github.com/mitchellh/reflectwalk"
	"github.com/zclconf/go-cty/cty"
)

// decodeMapInterfaceType decodes hcl instances of `map[string]interface{}` fields
// of v.
//
// The HCL parser stores the hcl AST as the map values, and decodeMapInterfaceType
// evaluates the AST and converts them to the native golang types.
func decodeMapInterfaceType(v interface{}, ctx *hcl.EvalContext) hcl.Diagnostics {
	w := &walker{ctx: ctx}
	err := reflectwalk.Walk(v, w)
	if err != nil {
		w.diags = append(w.diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "unexpected internal error",
			Detail:   err.Error(),
		})
	}
	return w.diags
}

type walker struct {
	ctx   *hcl.EvalContext
	diags hcl.Diagnostics
}

var mapStringInterfaceType = reflect.TypeOf(map[string]interface{}{})

func (w *walker) Map(m reflect.Value) error {
	if !m.Type().AssignableTo(mapStringInterfaceType) {
		return nil
	}

	// ignore private map fields
	if !m.CanSet() {
		return nil
	}

	for _, k := range m.MapKeys() {
		v := m.MapIndex(k)
		if attr, ok := v.Interface().(*hcl.Attribute); ok {
			c, diags := decodeInterface(attr.Expr, w.ctx)
			w.diags = append(w.diags, diags...)

			m.SetMapIndex(k, reflect.ValueOf(c))
		}
	}
	return nil
}

func (w *walker) MapElem(m, k, v reflect.Value) error {
	return nil
}
func decodeInterface(expr hcl.Expression, ctx *hcl.EvalContext) (interface{}, hcl.Diagnostics) {
	srvVal, diags := expr.Value(ctx)

	dst, err := interfaceFromCtyValue(srvVal)
	if err != nil {
		diags = append(diags, &hcl.Diagnostic{
			Severity: hcl.DiagError,
			Summary:  "unsuitable value type",
			Detail:   fmt.Sprintf("Unsuitable value: %s", err.Error()),
			Subject:  expr.StartRange().Ptr(),
			Context:  expr.Range().Ptr(),
		})
	}

	return dst, diags
}

func interfaceFromCtyValue(val cty.Value) (interface{}, error) {
	t := val.Type()

	if val.IsNull() {
		return nil, nil
	}

	if !val.IsKnown() {
		return nil, fmt.Errorf("value is not known")
	}

	// The caller should've guaranteed that the given val is conformant with
	// the given type t, so we'll proceed under that assumption here.

	switch {
	case t.IsPrimitiveType():
		switch t {
		case cty.String:
			return val.AsString(), nil
		case cty.Number:
			if val.RawEquals(cty.PositiveInfinity) {
				return math.Inf(1), nil
			} else if val.RawEquals(cty.NegativeInfinity) {
				return math.Inf(-1), nil
			} else {
				return smallestNumber(val.AsBigFloat()), nil
			}
		case cty.Bool:
			return val.True(), nil
		default:
			panic("unsupported primitive type")
		}
	case isCollectionOfMaps(t):
		result := []map[string]interface{}{}

		it := val.ElementIterator()
		for it.Next() {
			_, ev := it.Element()
			evi, err := interfaceFromCtyValue(ev)
			if err != nil {
				return nil, err
			}
			result = append(result, evi.(map[string]interface{}))
		}
		return result, nil
	case t.IsListType(), t.IsSetType(), t.IsTupleType():
		result := []interface{}{}

		it := val.ElementIterator()
		for it.Next() {
			_, ev := it.Element()
			evi, err := interfaceFromCtyValue(ev)
			if err != nil {
				return nil, err
			}
			result = append(result, evi)
		}
		return result, nil
	case t.IsMapType():
		result := map[string]interface{}{}
		it := val.ElementIterator()
		for it.Next() {
			ek, ev := it.Element()

			ekv := ek.AsString()
			evv, err := interfaceFromCtyValue(ev)
			if err != nil {
				return nil, err
			}

			result[ekv] = evv
		}
		return result, nil
	case t.IsObjectType():
		result := map[string]interface{}{}

		for k := range t.AttributeTypes() {
			av := val.GetAttr(k)
			avv, err := interfaceFromCtyValue(av)
			if err != nil {
				return nil, err
			}

			result[k] = avv
		}
		return result, nil
	case t.IsCapsuleType():
		rawVal := val.EncapsulatedValue()
		return rawVal, nil
	default:
		// should never happen
		return nil, fmt.Errorf("cannot serialize %s", t.FriendlyName())
	}
}

func isCollectionOfMaps(t cty.Type) bool {
	switch {
	case t.IsCollectionType():
		et := t.ElementType()
		return et.IsMapType() || et.IsObjectType()
	case t.IsTupleType():
		ets := t.TupleElementTypes()
		for _, et := range ets {
			if !et.IsMapType() && !et.IsObjectType() {
				return false
			}
		}

		return len(ets) > 0
	default:
		return false
	}
}

func smallestNumber(b *big.Float) interface{} {
	if v, acc := b.Int64(); acc == big.Exact {
		// check if it fits in int
		if int64(int(v)) == v {
			return int(v)
		}
		return v
	}

	v, _ := b.Float64()
	return v
}
