package orm

import (
	errorsmod "cosmossdk.io/errors"
	storetypes "cosmossdk.io/store/types"

	"git.cw.tr/mukan-network/mukan-sdk/x/group/errors"
)

// IndexerFunc creates one or multiple index keys for the source object.
type IndexerFunc func(value any) ([]any, error)

// IndexerFunc creates exactly one index key for the source object.
type UniqueIndexerFunc func(value any) (any, error)

// Indexer manages the persistence of an Index based on searchable keys and operations.
type Indexer struct {
	indexerFunc IndexerFunc
	addFunc     func(store storetypes.KVStore, secondaryIndexKey any, rowID RowID) error
}

// NewIndexer returns an indexer that supports multiple reference keys for an entity.
func NewIndexer(indexerFunc IndexerFunc) (*Indexer, error) {
	if indexerFunc == nil {
		return nil, errors.ErrORMInvalidArgument.Wrap("Indexer func must not be nil")
	}
	return &Indexer{
		indexerFunc: pruneEmptyKeys(indexerFunc),
		addFunc:     multiKeyAddFunc,
	}, nil
}

// NewUniqueIndexer returns an indexer that requires exactly one reference keys for an entity.
func NewUniqueIndexer(f UniqueIndexerFunc) (*Indexer, error) {
	if f == nil {
		return nil, errors.ErrORMInvalidArgument.Wrap("Indexer func must not be nil")
	}
	adaptor := func(indexerFunc UniqueIndexerFunc) IndexerFunc {
		return func(v any) ([]any, error) {
			k, err := indexerFunc(v)
			return []any{k}, err
		}
	}
	idx, err := NewIndexer(adaptor(f))
	if err != nil {
		return nil, err
	}
	idx.addFunc = uniqueKeysAddFunc
	return idx, nil
}

// IndexerFunc returns the indexer IndexerFunc,
// ensuring it has been prune from empty keys.
func (i Indexer) IndexerFunc() IndexerFunc {
	return i.indexerFunc
}

// OnCreate persists the secondary index entries for the new object.
func (i Indexer) OnCreate(store storetypes.KVStore, rowID RowID, value any) error {
	secondaryIndexKeys, err := i.indexerFunc(value)
	if err != nil {
		return err
	}

	for _, secondaryIndexKey := range secondaryIndexKeys {
		if err := i.addFunc(store, secondaryIndexKey, []byte(rowID)); err != nil {
			return err
		}
	}
	return nil
}

// OnDelete removes the secondary index entries for the deleted object.
func (i Indexer) OnDelete(store storetypes.KVStore, rowID RowID, value any) error {
	secondaryIndexKeys, err := i.indexerFunc(value)
	if err != nil {
		return err
	}

	for _, secondaryIndexKey := range secondaryIndexKeys {
		indexKey, err := buildKeyFromParts([]any{secondaryIndexKey, []byte(rowID)})
		if err != nil {
			return err
		}
		store.Delete(indexKey)
	}
	return nil
}

// OnUpdate rebuilds the secondary index entries for the updated object.
func (i Indexer) OnUpdate(store storetypes.KVStore, rowID RowID, newValue, oldValue any) error {
	oldSecIdxKeys, err := i.indexerFunc(oldValue)
	if err != nil {
		return err
	}
	newSecIdxKeys, err := i.indexerFunc(newValue)
	if err != nil {
		return err
	}
	oldKeys, err := difference(oldSecIdxKeys, newSecIdxKeys)
	if err != nil {
		return err
	}
	for _, oldIdxKey := range oldKeys {
		indexKey, err := buildKeyFromParts([]any{oldIdxKey, []byte(rowID)})
		if err != nil {
			return err
		}
		store.Delete(indexKey)
	}
	newKeys, err := difference(newSecIdxKeys, oldSecIdxKeys)
	if err != nil {
		return err
	}
	for _, newIdxKey := range newKeys {
		if err := i.addFunc(store, newIdxKey, rowID); err != nil {
			return err
		}
	}
	return nil
}

// uniqueKeysAddFunc enforces keys to be unique
func uniqueKeysAddFunc(store storetypes.KVStore, secondaryIndexKey any, rowID RowID) error {
	secondaryIndexKeyBytes, err := keyPartBytes(secondaryIndexKey, false)
	if err != nil {
		return err
	}
	if len(secondaryIndexKeyBytes) == 0 {
		return errorsmod.Wrap(errors.ErrORMInvalidArgument, "empty index key")
	}

	if err := checkUniqueIndexKey(store, secondaryIndexKeyBytes); err != nil {
		return err
	}

	indexKey, err := buildKeyFromParts([]any{secondaryIndexKey, []byte(rowID)})
	if err != nil {
		return err
	}

	store.Set(indexKey, []byte{})
	return nil
}

// checkUniqueIndexKey checks that the given secondary index key is unique
func checkUniqueIndexKey(store storetypes.KVStore, secondaryIndexKeyBytes []byte) error {
	it := store.Iterator(PrefixRange(secondaryIndexKeyBytes))
	defer it.Close()
	if it.Valid() {
		return errors.ErrORMUniqueConstraint
	}
	return nil
}

// multiKeyAddFunc allows multiple entries for a key
func multiKeyAddFunc(store storetypes.KVStore, secondaryIndexKey any, rowID RowID) error {
	secondaryIndexKeyBytes, err := keyPartBytes(secondaryIndexKey, false)
	if err != nil {
		return err
	}
	if len(secondaryIndexKeyBytes) == 0 {
		return errorsmod.Wrap(errors.ErrORMInvalidArgument, "empty index key")
	}

	encodedKey, err := buildKeyFromParts([]any{secondaryIndexKey, []byte(rowID)})
	if err != nil {
		return err
	}
	if len(encodedKey) == 0 {
		return errorsmod.Wrap(errors.ErrORMInvalidArgument, "empty index key")
	}

	store.Set(encodedKey, []byte{})
	return nil
}

// difference returns the list of elements that are in a but not in b.
func difference(a, b []any) ([]any, error) {
	set := make(map[any]struct{}, len(b))
	for _, v := range b {
		bt, err := keyPartBytes(v, true)
		if err != nil {
			return nil, err
		}
		set[string(bt)] = struct{}{}
	}
	var result []any
	for _, v := range a {
		bt, err := keyPartBytes(v, true)
		if err != nil {
			return nil, err
		}
		if _, ok := set[string(bt)]; !ok {
			result = append(result, v)
		}
	}
	return result, nil
}

// pruneEmptyKeys drops any empty key from IndexerFunc f returned
func pruneEmptyKeys(f IndexerFunc) IndexerFunc {
	return func(v any) ([]any, error) {
		keys, err := f(v)
		if err != nil || keys == nil {
			return keys, err
		}
		r := make([]any, 0, len(keys))
		for i := range keys {
			key, err := keyPartBytes(keys[i], true)
			if err != nil {
				return nil, err
			}
			if len(key) != 0 {
				r = append(r, keys[i])
			}
		}
		return r, nil
	}
}
