package json

import (
	"bytes"
	"encoding/json"
	"fmt"
	"reflect"

	"github.com/hashicorp/go-cty/cty"
	"github.com/hashicorp/go-cty/cty/convert"
)

// unmarshalOptions configures options for the JSON to CTY decoder.
type unmarshalOptions struct {
	ImpliedDynamic bool
}

func unmarshal(buf []byte, t cty.Type, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	dec := bufDecoder(buf)

	tok, err := dec.Token()
	if err != nil {
		return cty.NilVal, path.NewError(err)
	}

	if tok == nil {
		return cty.NullVal(t), nil
	}

	switch {
	case t.IsPrimitiveType():
		val, err := unmarshalPrimitive(tok, t, path, opt)
		if err != nil {
			return cty.NilVal, err
		}
		return val, nil
	case t.IsListType():
		return unmarshalList(buf, t.ElementType(), path, opt)
	case t.IsSetType():
		return unmarshalSet(buf, t.ElementType(), path, opt)
	case t.IsMapType():
		return unmarshalMap(buf, t.ElementType(), path, opt)
	case t.IsTupleType():
		return unmarshalTuple(buf, t.TupleElementTypes(), path, opt)
	case t.IsObjectType():
		return unmarshalObject(buf, t.AttributeTypes(), path, opt)
	case t.IsCapsuleType():
		return unmarshalCapsule(buf, t, path, opt)
	case t.Equals(cty.DynamicPseudoType):
		if opt.ImpliedDynamic {
			return unmarshalIntoDynamic(buf, path, opt)
		} else {
			return unmarshalDynamic(buf, path, opt)
		}
	default:
		return cty.NilVal, path.NewErrorf("unsupported type %s", t.FriendlyName())
	}
}

func unmarshalPrimitive(tok json.Token, t cty.Type, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {

	switch t {
	case cty.Bool:
		switch v := tok.(type) {
		case bool:
			return cty.BoolVal(v), nil
		case string:
			val, err := convert.Convert(cty.StringVal(v), t)
			if err != nil {
				return cty.NilVal, path.NewError(err)
			}
			return val, nil
		default:
			return cty.NilVal, path.NewErrorf("bool is required")
		}
	case cty.Number:
		if v, ok := tok.(json.Number); ok {
			tok = string(v)
		}
		switch v := tok.(type) {
		case string:
			val, err := cty.ParseNumberVal(v)
			if err != nil {
				return cty.NilVal, path.NewError(err)
			}
			return val, nil
		default:
			return cty.NilVal, path.NewErrorf("number is required")
		}
	case cty.String:
		switch v := tok.(type) {
		case string:
			return cty.StringVal(v), nil
		case json.Number:
			return cty.StringVal(string(v)), nil
		case bool:
			val, err := convert.Convert(cty.BoolVal(v), t)
			if err != nil {
				return cty.NilVal, path.NewError(err)
			}
			return val, nil
		default:
			return cty.NilVal, path.NewErrorf("string is required")
		}
	default:
		// should never happen
		panic("unsupported primitive type")
	}
}

func unmarshalList(buf []byte, ety cty.Type, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	dec := bufDecoder(buf)
	if err := requireDelim(dec, '['); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	var vals []cty.Value

	{
		path := append(path, nil)
		var idx int64

		for dec.More() {
			path[len(path)-1] = cty.IndexStep{
				Key: cty.NumberIntVal(idx),
			}
			idx++

			rawVal, err := readRawValue(dec)
			if err != nil {
				return cty.NilVal, path.NewErrorf("failed to read list value: %s", err)
			}

			el, err := unmarshal(rawVal, ety, path, opt)
			if err != nil {
				return cty.NilVal, err
			}

			vals = append(vals, el)
		}
	}

	if err := requireDelim(dec, ']'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

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

	return cty.ListVal(vals), nil
}

func unmarshalSet(buf []byte, ety cty.Type, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	dec := bufDecoder(buf)
	if err := requireDelim(dec, '['); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	var vals []cty.Value

	{
		path := append(path, nil)

		for dec.More() {
			path[len(path)-1] = cty.IndexStep{
				Key: cty.UnknownVal(ety),
			}

			rawVal, err := readRawValue(dec)
			if err != nil {
				return cty.NilVal, path.NewErrorf("failed to read set value: %s", err)
			}

			el, err := unmarshal(rawVal, ety, path, opt)
			if err != nil {
				return cty.NilVal, err
			}

			vals = append(vals, el)
		}
	}

	if err := requireDelim(dec, ']'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

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

	return cty.SetVal(vals), nil
}

func unmarshalMap(buf []byte, ety cty.Type, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	dec := bufDecoder(buf)
	if err := requireDelim(dec, '{'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	vals := make(map[string]cty.Value)

	{
		path := append(path, nil)

		for dec.More() {
			path[len(path)-1] = cty.IndexStep{
				Key: cty.UnknownVal(cty.String),
			}

			var err error

			k, err := requireObjectKey(dec)
			if err != nil {
				return cty.NilVal, path.NewErrorf("failed to read map key: %s", err)
			}

			path[len(path)-1] = cty.IndexStep{
				Key: cty.StringVal(k),
			}

			rawVal, err := readRawValue(dec)
			if err != nil {
				return cty.NilVal, path.NewErrorf("failed to read map value: %s", err)
			}

			el, err := unmarshal(rawVal, ety, path, opt)
			if err != nil {
				return cty.NilVal, err
			}

			vals[k] = el
		}
	}

	if err := requireDelim(dec, '}'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

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

	return cty.MapVal(vals), nil
}

func unmarshalTuple(buf []byte, etys []cty.Type, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	dec := bufDecoder(buf)
	if err := requireDelim(dec, '['); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	var vals []cty.Value

	{
		path := append(path, nil)
		var idx int

		for dec.More() {
			if idx >= len(etys) {
				return cty.NilVal, path[:len(path)-1].NewErrorf("too many tuple elements (need %d)", len(etys))
			}

			path[len(path)-1] = cty.IndexStep{
				Key: cty.NumberIntVal(int64(idx)),
			}
			ety := etys[idx]
			idx++

			rawVal, err := readRawValue(dec)
			if err != nil {
				return cty.NilVal, path.NewErrorf("failed to read tuple value: %s", err)
			}

			el, err := unmarshal(rawVal, ety, path, opt)
			if err != nil {
				return cty.NilVal, err
			}

			vals = append(vals, el)
		}
	}

	if err := requireDelim(dec, ']'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	if len(vals) != len(etys) {
		return cty.NilVal, path[:len(path)-1].NewErrorf("not enough tuple elements (need %d)", len(etys))
	}

	if len(vals) == 0 {
		return cty.EmptyTupleVal, nil
	}

	return cty.TupleVal(vals), nil
}

func unmarshalObject(buf []byte, atys map[string]cty.Type, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	dec := bufDecoder(buf)
	if err := requireDelim(dec, '{'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	vals := make(map[string]cty.Value)

	{
		objPath := path           // some errors report from the object's perspective
		path := append(path, nil) // path to a specific attribute

		for dec.More() {

			var err error

			k, err := requireObjectKey(dec)
			if err != nil {
				return cty.NilVal, path.NewErrorf("failed to read object key: %s", err)
			}

			aty, ok := atys[k]
			if !ok {
				return cty.NilVal, objPath.NewErrorf("unsupported attribute %q", k)
			}

			path[len(path)-1] = cty.GetAttrStep{
				Name: k,
			}

			rawVal, err := readRawValue(dec)
			if err != nil {
				return cty.NilVal, path.NewErrorf("failed to read object value: %s", err)
			}

			el, err := unmarshal(rawVal, aty, path, opt)
			if err != nil {
				return cty.NilVal, err
			}

			vals[k] = el
		}
	}

	if err := requireDelim(dec, '}'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	// Make sure we have a value for every attribute
	for k, aty := range atys {
		if _, exists := vals[k]; !exists {
			vals[k] = cty.NullVal(aty)
		}
	}

	if len(vals) == 0 {
		return cty.EmptyObjectVal, nil
	}

	return cty.ObjectVal(vals), nil
}

func unmarshalCapsule(buf []byte, t cty.Type, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	rawType := t.EncapsulatedType()
	ptrPtr := reflect.New(reflect.PtrTo(rawType))
	ptrPtr.Elem().Set(reflect.New(rawType))
	ptr := ptrPtr.Elem().Interface()
	err := json.Unmarshal(buf, ptr)
	if err != nil {
		return cty.NilVal, path.NewError(err)
	}

	return cty.CapsuleVal(t, ptr), nil
}

func unmarshalDynamic(buf []byte, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	dec := bufDecoder(buf)
	if err := requireDelim(dec, '{'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	var t cty.Type
	var valBody []byte // defer actual decoding until we know the type

	for dec.More() {
		var err error

		key, err := requireObjectKey(dec)
		if err != nil {
			return cty.NilVal, path.NewErrorf("failed to read dynamic type descriptor key: %s", err)
		}

		rawVal, err := readRawValue(dec)
		if err != nil {
			return cty.NilVal, path.NewErrorf("failed to read dynamic type descriptor value: %s", err)
		}

		switch key {
		case "type":
			err := json.Unmarshal(rawVal, &t)
			if err != nil {
				return cty.NilVal, path.NewErrorf("failed to decode type for dynamic value: %s", err)
			}
		case "value":
			valBody = rawVal
		default:
			return cty.NilVal, path.NewErrorf("invalid key %q in dynamically-typed value", key)
		}

	}

	if err := requireDelim(dec, '}'); err != nil {
		return cty.NilVal, path.NewError(err)
	}

	if t == cty.NilType {
		return cty.NilVal, path.NewErrorf("missing type in dynamically-typed value")
	}
	if valBody == nil {
		return cty.NilVal, path.NewErrorf("missing value in dynamically-typed value")
	}

	val, err := unmarshal([]byte(valBody), t, cty.Path{}, opt)
	if err != nil {
		return cty.NilVal, path.NewError(err)
	}
	return val, nil
}

func requireDelim(dec *json.Decoder, d rune) error {
	tok, err := dec.Token()
	if err != nil {
		return err
	}

	if tok != json.Delim(d) {
		return fmt.Errorf("missing expected %c", d)
	}

	return nil
}

func requireObjectKey(dec *json.Decoder) (string, error) {
	tok, err := dec.Token()
	if err != nil {
		return "", err
	}
	if s, ok := tok.(string); ok {
		return s, nil
	}
	return "", fmt.Errorf("missing expected object key")
}

func readRawValue(dec *json.Decoder) ([]byte, error) {
	var rawVal json.RawMessage
	err := dec.Decode(&rawVal)
	if err != nil {
		return nil, err
	}
	return []byte(rawVal), nil
}

func bufDecoder(buf []byte) *json.Decoder {
	r := bytes.NewReader(buf)
	dec := json.NewDecoder(r)
	dec.UseNumber()
	return dec
}

func unmarshalIntoDynamic(buf []byte, path cty.Path, opt *unmarshalOptions) (cty.Value, error) {
	var t cty.Type
	var err error

	if buf == nil {
		return cty.NilVal, path.NewErrorf("missing value in dynamically-typed value")
	}

	t, err = ImpliedType(buf)
	if err != nil {
		return cty.NilVal, path.NewErrorf("failed to imply dynamic type: %s", err)
	}

	if t == cty.NilType {
		return cty.NilVal, path.NewErrorf("missing type in dynamically-typed value")
	}

	val, err := unmarshal(buf, t, path, opt)
	if err != nil {
		return cty.NilVal, path.NewError(err)
	}
	return val, nil
}
