package cmdpacks

import (
	"fmt"
	"maps"
	"strconv"

	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
	"github.com/pkg/errors"
	"lmika.dev/cmd/dynamo-browse/internal/dynamo-browse/models"
	"ucl.lmika.dev/ucl"
)

type proxyInfo[T comparable] struct {
	fields  map[string]func(t T) ucl.Object
	lenFunc func(t T) int
	strFunc func(t T) string
}

type SimpleProxy[T comparable] struct {
	value     T
	proxyInfo *proxyInfo[T]
}

func (tp SimpleProxy[T]) ProxyValue() T {
	return tp.value
}

func (tp SimpleProxy[T]) String() string {
	if tp.proxyInfo.strFunc != nil {
		return tp.proxyInfo.strFunc(tp.value)
	}
	return fmt.Sprint(tp.value)
}

func (tp SimpleProxy[T]) Truthy() bool {
	var zeroT T
	return tp.value != zeroT
}

func (tp SimpleProxy[T]) Len() int {
	if tp.proxyInfo.lenFunc != nil {
		return tp.proxyInfo.lenFunc(tp.value)
	}
	return len(tp.proxyInfo.fields)
}

func (tp SimpleProxy[T]) Value(k string) ucl.Object {
	f, ok := tp.proxyInfo.fields[k]
	if !ok {
		return nil
	}
	return f(tp.value)
}

func (tp SimpleProxy[T]) Each(fn func(k string, v ucl.Object) error) error {
	for key := range maps.Keys(tp.proxyInfo.fields) {
		if err := fn(key, tp.Value(key)); err != nil {
			return err
		}
	}
	return nil
}

type simpleProxyList[T comparable] struct {
	values    []T
	converter func(T) ucl.Object
}

func newSimpleProxyList[T comparable](values []T, converter func(T) ucl.Object) simpleProxyList[T] {
	return simpleProxyList[T]{values: values, converter: converter}
}

func (tp simpleProxyList[T]) String() string {
	return fmt.Sprint(tp.values)
}

func (tp simpleProxyList[T]) Truthy() bool {
	return len(tp.values) > 0
}

func (tp simpleProxyList[T]) Len() int {
	return len(tp.values)
}

func (tp simpleProxyList[T]) Index(k int) ucl.Object {
	return tp.converter(tp.values[k])
}

func newResultSetProxy(rs *models.ResultSet) ucl.Object {
	return SimpleProxy[*models.ResultSet]{value: rs, proxyInfo: resultSetProxyFields}
}

var resultSetProxyFields = &proxyInfo[*models.ResultSet]{
	lenFunc: func(t *models.ResultSet) int { return len(t.Items()) },
	strFunc: func(t *models.ResultSet) string {
		return fmt.Sprintf("ResultSet(%v:%d)", t.TableInfo.Name, len(t.Items()))
	},
	fields: map[string]func(t *models.ResultSet) ucl.Object{
		"Table":       func(t *models.ResultSet) ucl.Object { return newTableProxy(t.TableInfo) },
		"Items":       func(t *models.ResultSet) ucl.Object { return resultSetItemsProxy{t} },
		"HasNextPage": func(t *models.ResultSet) ucl.Object { return ucl.BoolObject(t.HasNextPage()) },
		"First": func(t *models.ResultSet) ucl.Object {
			items := t.Items()
			if len(items) == 0 {
				return nil
			}
			return itemProxy{resultSet: t, idx: 0, item: items[0]}
		},
	},
}

func newTableProxy(table *models.TableInfo) ucl.Object {
	return SimpleProxy[*models.TableInfo]{value: table, proxyInfo: tableProxyFields}
}

var tableProxyFields = &proxyInfo[*models.TableInfo]{
	strFunc: func(t *models.TableInfo) string {
		return fmt.Sprintf("Table(%v)", t.Name)
	},
	fields: map[string]func(t *models.TableInfo) ucl.Object{
		"Name":              func(t *models.TableInfo) ucl.Object { return ucl.StringObject(t.Name) },
		"Keys":              func(t *models.TableInfo) ucl.Object { return newKeyAttributeProxy(t.Keys) },
		"DefinedAttributes": func(t *models.TableInfo) ucl.Object { return ucl.StringListObject(t.DefinedAttributes) },
		"GSIs":              func(t *models.TableInfo) ucl.Object { return newSimpleProxyList(t.GSIs, newGSIProxy) },
	},
}

func newKeyAttributeProxy(keyAttrs models.KeyAttribute) ucl.Object {
	return SimpleProxy[models.KeyAttribute]{value: keyAttrs, proxyInfo: keyAttributeProxyFields}
}

var keyAttributeProxyFields = &proxyInfo[models.KeyAttribute]{
	strFunc: func(t models.KeyAttribute) string {
		return fmt.Sprintf("KeyAttribute(%v,%v)", t.PartitionKey, t.SortKey)
	},
	fields: map[string]func(t models.KeyAttribute) ucl.Object{
		"PK": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.PartitionKey) },
		"SK": func(t models.KeyAttribute) ucl.Object { return ucl.StringObject(t.SortKey) },
	},
}

func newGSIProxy(gsi models.TableGSI) ucl.Object {
	return SimpleProxy[models.TableGSI]{value: gsi, proxyInfo: gsiProxyFields}
}

var gsiProxyFields = &proxyInfo[models.TableGSI]{
	strFunc: func(t models.TableGSI) string {
		return fmt.Sprintf("TableGSI(%v,(%v,%v))", t.Name, t.Keys.PartitionKey, t.Keys.SortKey)
	},
	fields: map[string]func(t models.TableGSI) ucl.Object{
		"Name": func(t models.TableGSI) ucl.Object { return ucl.StringObject(t.Name) },
		"Keys": func(t models.TableGSI) ucl.Object { return newKeyAttributeProxy(t.Keys) },
	},
}

type resultSetItemsProxy struct {
	resultSet *models.ResultSet
}

func (ip resultSetItemsProxy) String() string {
	return "RSItem()"
}

func (ip resultSetItemsProxy) Truthy() bool {
	return len(ip.resultSet.Items()) > 0
}

func (tp resultSetItemsProxy) Len() int {
	return len(tp.resultSet.Items())
}

func (tp resultSetItemsProxy) Index(k int) ucl.Object {
	return itemProxy{resultSet: tp.resultSet, idx: k, item: tp.resultSet.Items()[k]}
}

type resultSetMarkedItemsProxy struct {
	resultSet *models.ResultSet
}

func (ip resultSetMarkedItemsProxy) String() string {
	return fmt.Sprintf("MarkedItems(%v)", len(ip.resultSet.MarkedItems()))
}

func (ip resultSetMarkedItemsProxy) Truthy() bool {
	return len(ip.resultSet.MarkedItems()) > 0
}

func (tp resultSetMarkedItemsProxy) Len() int {
	return len(tp.resultSet.MarkedItems())
}

func (tp resultSetMarkedItemsProxy) Index(k int) ucl.Object {
	markedItems := tp.resultSet.MarkedItems()
	if k >= len(markedItems) {
		return nil
	}
	actualItem := tp.resultSet.Items()[markedItems[k].Index]
	return itemProxy{resultSet: tp.resultSet, idx: markedItems[k].Index, item: actualItem}
}

type itemProxy struct {
	resultSet *models.ResultSet
	idx       int
	item      models.Item
}

func (ip itemProxy) String() string {
	return fmt.Sprintf("RSItems(%v)", len(ip.item))
}

func (ip itemProxy) Truthy() bool {
	return len(ip.item) > 0
}

func (tp itemProxy) Len() int {
	return len(tp.item)
}

func (tp itemProxy) Value(k string) ucl.Object {
	f, ok := tp.item[k]
	if !ok {
		return nil
	}
	return convertAttributeValueToUCLObject(f)
}

func (tp itemProxy) Each(fn func(k string, v ucl.Object) error) error {
	for key := range maps.Keys(tp.item) {
		if err := fn(key, tp.Value(key)); err != nil {
			return err
		}
	}
	return nil
}

type attrPathProxy struct {
	attrPath *models.AttrPathNode
}

func (ap attrPathProxy) String() string {
	return "RSItem()"
}

func (ap attrPathProxy) Truthy() bool {
	return true
}

func (ap attrPathProxy) Len() (l int) {
	for p := ap.attrPath; p != nil; p = p.Parent {
		l++
	}
	return
}

func (ap attrPathProxy) Index(k int) ucl.Object {
	if k == -1 {
		return ucl.StringObject(ap.attrPath.Key)
	}

	if k >= 0 {
		k = ap.Len() - k - 1
	} else {
		k = -k - 1
	}

	if k < 0 {
		return nil
	}
	for p := ap.attrPath; p != nil; p = p.Parent {
		if k <= 0 {
			return ucl.StringObject(p.Key)
		}
		k -= 1
	}

	return nil
}

type attributeValueProxy struct {
	value types.AttributeValue
}

func (ip attributeValueProxy) String() string {
	return "attributeValueProxy()"
}

func (ip attributeValueProxy) Truthy() bool {
	return ip.value != nil
}

func convertAttributeValueToUCLObject(attrValue types.AttributeValue) ucl.Object {
	switch t := attrValue.(type) {
	case *types.AttributeValueMemberS:
		return ucl.StringObject(t.Value)
	case *types.AttributeValueMemberN:
		i, err := strconv.ParseInt(t.Value, 10, 64)
		if err != nil {
			return nil
		}
		return ucl.IntObject(i)
	case *types.AttributeValueMemberBOOL:
		return ucl.BoolObject(t.Value)
	case *types.AttributeValueMemberL:
		vs := make(ucl.ListObject, len(t.Value))
		for i, v := range t.Value {
			vs[i] = convertAttributeValueToUCLObject(v)
		}
		return &vs
	case *types.AttributeValueMemberM:
		hs := make(ucl.HashObject)
		for k, v := range t.Value {
			hs[k] = convertAttributeValueToUCLObject(v)
		}
		return hs
	}

	return attributeValueProxy{value: attrValue}
}

func mapUCLObjectToAttributeType(obj ucl.Object) (types.AttributeValue, error) {
	switch t := obj.(type) {
	case ucl.StringObject:
		return &types.AttributeValueMemberS{Value: t.String()}, nil
	case ucl.IntObject:
		return &types.AttributeValueMemberN{Value: t.String()}, nil
	case ucl.BoolObject:
		return &types.AttributeValueMemberBOOL{Value: t.Truthy()}, nil
	case ucl.Listable:
		vals := make([]types.AttributeValue, t.Len())
		for i := 0; i < t.Len(); i++ {
			v, err := mapUCLObjectToAttributeType(t.Index(i))
			if err != nil {
				return nil, err
			}
			vals[i] = v
		}
		return &types.AttributeValueMemberL{Value: vals}, nil
	case ucl.Hashable:
		vals := make(map[string]types.AttributeValue)
		if err := t.Each(func(k string, v ucl.Object) error {
			vv, err := mapUCLObjectToAttributeType(v)
			if err != nil {
				return err
			}
			vals[k] = vv
			return nil
		}); err != nil {
			return nil, err
		}
		return &types.AttributeValueMemberM{Value: vals}, nil
	case attributeValueProxy:
		return t.value, nil
	}
	return nil, errors.New("unsupported attribute type")
}
