package storage

import (
	"bytes"
	"context"
	"fmt"
	"slices"

	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

// Storage iterator options.
const (
	FindDefault      = 0
	FindKeysOnly     = 1 << 0
	FindRemovePrefix = 1 << 1
	FindValuesOnly   = 1 << 2
	FindDeserialize  = 1 << 3
	FindPick0        = 1 << 4
	FindPick1        = 1 << 5
	FindBackwards    = 1 << 7

	FindAll = FindDefault | FindKeysOnly | FindRemovePrefix | FindValuesOnly |
		FindDeserialize | FindPick0 | FindPick1 | FindBackwards
)

// Iterator is an iterator state representation.
type Iterator struct {
	seekCh chan storage.KeyValue
	curr   storage.KeyValue
	next   bool
	opts   int64
	// prefix is the storage item key prefix Find is performed with. It must be
	// copied if no FindRemovePrefix option specified since it's shared between all
	// iterator items.
	prefix []byte
}

// NewIterator creates a new Iterator with the given options for the given channel of store.Seek results.
func NewIterator(seekCh chan storage.KeyValue, prefix []byte, opts int64) *Iterator {
	return &Iterator{
		seekCh: seekCh,
		opts:   opts,
		prefix: bytes.Clone(prefix),
	}
}

// Next advances the iterator and returns true if Value can be called at the
// current position.
func (s *Iterator) Next() bool {
	s.curr, s.next = <-s.seekCh
	return s.next
}

// Value returns current iterators value (exact type depends on options this
// iterator was created with).
func (s *Iterator) Value() stackitem.Item {
	if !s.next {
		panic("iterator index out of range")
	}
	key := s.curr.Key
	if s.opts&FindRemovePrefix == 0 {
		key = slices.Concat(s.prefix, key)
	}
	if s.opts&FindKeysOnly != 0 {
		return stackitem.NewByteArray(key)
	}
	value := stackitem.Item(stackitem.NewByteArray(s.curr.Value))
	if s.opts&FindDeserialize != 0 {
		bs := s.curr.Value
		var err error
		value, err = stackitem.Deserialize(bs)
		if err != nil {
			panic(err)
		}
	}
	if s.opts&FindPick0 != 0 {
		value = value.Value().([]stackitem.Item)[0]
	} else if s.opts&FindPick1 != 0 {
		value = value.Value().([]stackitem.Item)[1]
	}
	if s.opts&FindValuesOnly != 0 {
		return value
	}
	return stackitem.NewStruct([]stackitem.Item{
		stackitem.NewByteArray(key),
		value,
	})
}

func findWithContext(ic *interop.Context, stc *Context) error {
	prefix := ic.VM.Estack().Pop().Bytes()
	opts := ic.VM.Estack().Pop().BigInt().Int64()
	if opts&^FindAll != 0 {
		return fmt.Errorf("%w: unknown flag", errFindInvalidOptions)
	}
	if opts&FindKeysOnly != 0 &&
		opts&(FindDeserialize|FindPick0|FindPick1) != 0 {
		return fmt.Errorf("%w KeysOnly conflicts with other options", errFindInvalidOptions)
	}
	if opts&FindValuesOnly != 0 &&
		opts&(FindKeysOnly|FindRemovePrefix) != 0 {
		return fmt.Errorf("%w: KeysOnly conflicts with ValuesOnly", errFindInvalidOptions)
	}
	if opts&FindPick0 != 0 && opts&FindPick1 != 0 {
		return fmt.Errorf("%w: Pick0 conflicts with Pick1", errFindInvalidOptions)
	}
	if opts&FindDeserialize == 0 && (opts&FindPick0 != 0 || opts&FindPick1 != 0) {
		return fmt.Errorf("%w: PickN is specified without Deserialize", errFindInvalidOptions)
	}
	bkwrds := opts&FindBackwards != 0
	ctx, cancel := context.WithCancel(context.Background())
	seekres := ic.DAO.SeekAsync(ctx, stc.ID, storage.SeekRange{Prefix: prefix, Backwards: bkwrds})
	item := NewIterator(seekres, prefix, opts)
	ic.VM.Estack().PushItem(stackitem.NewInterop(item))
	ic.RegisterCancelFunc(func() {
		cancel()
		// Underlying persistent store is likely to be a private MemCachedStore. Thus,
		// to avoid concurrent map iteration and map write we need to wait until internal
		// seek goroutine is finished, because it can access underlying persistent store.
		for range seekres { //nolint:revive //empty-block
		}
	})

	return nil
}

// Find finds stored key-value pair.
func Find(ic *interop.Context) error {
	stcInterface := ic.VM.Estack().Pop().Value()
	stc, ok := stcInterface.(*Context)
	if !ok {
		return fmt.Errorf("%T is not a storage.Context", stcInterface)
	}
	return findWithContext(ic, stc)
}

// LocalFind is similar to Find, but does not require storage context.
func LocalFind(ic *interop.Context) error {
	contract, err := ic.GetContract(ic.VM.GetCurrentScriptHash())
	if err != nil {
		return fmt.Errorf("storage context can not be retrieved in dynamic scripts: %w", err)
	}
	return findWithContext(ic, &Context{
		ID:       contract.ID,
		ReadOnly: true,
	})
}
