package indexes

import (
	"context"

	"cosmossdk.io/collections"
	"cosmossdk.io/collections/codec"
)

type reversePairOptions struct {
	uncheckedValue bool
}

// WithReversePairUncheckedValue is an option that can be passed to NewReversePair to
// ignore index values different from '[]byte{}' and continue with the operation.
// This should be used only if you are migrating to collections and have used a different
// placeholder value in your storage index keys.
// Refer to WithKeySetUncheckedValue for more information.
func WithReversePairUncheckedValue() func(*reversePairOptions) {
	return func(o *reversePairOptions) {
		o.uncheckedValue = true
	}
}

// ReversePair is an index that is used with collections.Pair keys. It indexes objects by their second part of the key.
// When the value is being indexed by collections.IndexedMap then ReversePair will create a relationship between
// the second part of the primary key and the first part.
type ReversePair[K1, K2, Value any] struct {
	refKeys collections.KeySet[collections.Pair[K2, K1]] // refKeys has the relationships between Join(K2, K1)
}

// TODO(tip): this is an interface to cast a collections.KeyCodec
// to a pair codec. currently we return it as a KeyCodec[Pair[K1, K2]]
// to improve dev experience with type inference, which means we cannot
// get the concrete implementation which exposes KeyCodec1 and KeyCodec2.
type pairKeyCodec[K1, K2 any] interface {
	KeyCodec1() codec.KeyCodec[K1]
	KeyCodec2() codec.KeyCodec[K2]
}

// NewReversePair instantiates a new ReversePair index.
// NOTE: when using this function you will need to type hint: doing NewReversePair[Value]()
// Example: if the value of the indexed map is string, you need to do NewReversePair[string](...)
func NewReversePair[Value, K1, K2 any](
	sb *collections.SchemaBuilder,
	prefix collections.Prefix,
	name string,
	pairCodec codec.KeyCodec[collections.Pair[K1, K2]],
	options ...func(*reversePairOptions),
) *ReversePair[K1, K2, Value] {
	pkc := pairCodec.(pairKeyCodec[K1, K2])
	o := new(reversePairOptions)
	for _, option := range options {
		option(o)
	}
	if o.uncheckedValue {
		return &ReversePair[K1, K2, Value]{
			refKeys: collections.NewKeySet(
				sb,
				prefix,
				name,
				collections.PairKeyCodec(pkc.KeyCodec2(), pkc.KeyCodec1()),
				collections.WithKeySetUncheckedValue(),
				collections.WithKeySetSecondaryIndex(),
			),
		}
	}

	mi := &ReversePair[K1, K2, Value]{
		refKeys: collections.NewKeySet(
			sb,
			prefix,
			name,
			collections.PairKeyCodec(pkc.KeyCodec2(), pkc.KeyCodec1()),
			collections.WithKeySetSecondaryIndex(),
		),
	}

	return mi
}

// Iterate exposes the raw iterator API.
func (i *ReversePair[K1, K2, Value]) Iterate(ctx context.Context, ranger collections.Ranger[collections.Pair[K2, K1]]) (iter ReversePairIterator[K2, K1], err error) {
	sIter, err := i.refKeys.Iterate(ctx, ranger)
	if err != nil {
		return
	}
	return (ReversePairIterator[K2, K1])(sIter), nil
}

// MatchExact will return an iterator containing only the primary keys starting with the provided second part of the multipart pair key.
func (i *ReversePair[K1, K2, Value]) MatchExact(ctx context.Context, key K2) (ReversePairIterator[K2, K1], error) {
	return i.Iterate(ctx, collections.NewPrefixedPairRange[K2, K1](key))
}

// Reference implements collections.Index
func (i *ReversePair[K1, K2, Value]) Reference(ctx context.Context, pk collections.Pair[K1, K2], _ Value, _ func() (Value, error)) error {
	return i.refKeys.Set(ctx, collections.Join(pk.K2(), pk.K1()))
}

// Unreference implements collections.Index
func (i *ReversePair[K1, K2, Value]) Unreference(ctx context.Context, pk collections.Pair[K1, K2], _ func() (Value, error)) error {
	return i.refKeys.Remove(ctx, collections.Join(pk.K2(), pk.K1()))
}

func (i *ReversePair[K1, K2, Value]) Walk(
	ctx context.Context,
	ranger collections.Ranger[collections.Pair[K2, K1]],
	walkFunc func(indexingKey K2, indexedKey K1) (stop bool, err error),
) error {
	return i.refKeys.Walk(ctx, ranger, func(key collections.Pair[K2, K1]) (bool, error) {
		return walkFunc(key.K1(), key.K2())
	})
}

func (i *ReversePair[K1, K2, Value]) IterateRaw(
	ctx context.Context, start, end []byte, order collections.Order,
) (
	iter collections.Iterator[collections.Pair[K2, K1], collections.NoValue], err error,
) {
	return i.refKeys.IterateRaw(ctx, start, end, order)
}

func (i *ReversePair[K1, K2, Value]) KeyCodec() codec.KeyCodec[collections.Pair[K2, K1]] {
	return i.refKeys.KeyCodec()
}

// ReversePairIterator is a helper type around a collections.KeySetIterator when used to work
// with ReversePair indexes iterations.
type ReversePairIterator[K2, K1 any] collections.KeySetIterator[collections.Pair[K2, K1]]

// PrimaryKey returns the primary key from the index. The index is composed like a reverse
// pair key. So we just fetch the pair key from the index and return the reverse.
func (m ReversePairIterator[K2, K1]) PrimaryKey() (pair collections.Pair[K1, K2], err error) {
	reversePair, err := m.FullKey()
	if err != nil {
		return pair, err
	}
	pair = collections.Join(reversePair.K2(), reversePair.K1())
	return pair, nil
}

// PrimaryKeys returns all the primary keys contained in the iterator.
func (m ReversePairIterator[K2, K1]) PrimaryKeys() (pairs []collections.Pair[K1, K2], err error) {
	defer m.Close()
	for ; m.Valid(); m.Next() {
		pair, err := m.PrimaryKey()
		if err != nil {
			return nil, err
		}
		pairs = append(pairs, pair)
	}
	return pairs, err
}

func (m ReversePairIterator[K2, K1]) FullKey() (p collections.Pair[K2, K1], err error) {
	return (collections.KeySetIterator[collections.Pair[K2, K1]])(m).Key()
}

func (m ReversePairIterator[K2, K1]) Next() {
	(collections.KeySetIterator[collections.Pair[K2, K1]])(m).Next()
}

func (m ReversePairIterator[K2, K1]) Valid() bool {
	return (collections.KeySetIterator[collections.Pair[K2, K1]])(m).Valid()
}

func (m ReversePairIterator[K2, K1]) Close() error {
	return (collections.KeySetIterator[collections.Pair[K2, K1]])(m).Close()
}
