package state

import (
	"crypto/elliptic"
	"errors"
	"fmt"
	"math/big"

	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/bigint"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// NEP17Balance represents the balance state of a NEP-17-token.
type NEP17Balance struct {
	Balance big.Int
}

// NEOBalance represents the balance state of a NEO-token.
type NEOBalance struct {
	NEP17Balance
	BalanceHeight  uint32
	VoteTo         *keys.PublicKey
	LastGasPerVote big.Int
}

// NEP17BalanceFromBytes converts the serialized NEP17Balance to a structure.
func NEP17BalanceFromBytes(b []byte) (*NEP17Balance, error) {
	if len(b) < 4 {
		if len(b) == 0 {
			return new(NEP17Balance), nil
		}
		return nil, errors.New("invalid format")
	}
	if b[0] != byte(stackitem.StructT) {
		return nil, errors.New("not a struct")
	}
	if b[1] != 1 {
		return nil, errors.New("invalid item count")
	}
	if st := stackitem.Type(b[2]); st != stackitem.IntegerT {
		return nil, fmt.Errorf("invalid balance: %s", st)
	}
	if int(b[3]) != len(b[4:]) {
		return nil, errors.New("invalid balance format")
	}
	return &NEP17Balance{Balance: *bigint.FromBytes(b[4:])}, nil
}

// Bytes returns serialized NEP17Balance.
func (s *NEP17Balance) Bytes(buf []byte) []byte {
	if cap(buf) < 4+bigint.MaxBytesLen {
		buf = make([]byte, 4, 4+bigint.MaxBytesLen)
	} else {
		buf = buf[:4]
	}
	buf[0] = byte(stackitem.StructT)
	buf[1] = 1
	buf[2] = byte(stackitem.IntegerT)

	data := bigint.ToPreallocatedBytes(&s.Balance, buf[4:])
	buf[3] = byte(len(data)) // max is 33, so we are ok here
	buf = append(buf, data...)
	return buf
}

func balanceFromBytes(b []byte, item stackitem.Convertible) error {
	if len(b) == 0 {
		return nil
	}
	return stackitem.DeserializeConvertible(b, item)
}

// ToStackItem implements stackitem.Convertible. It never returns an error.
func (s *NEP17Balance) ToStackItem() (stackitem.Item, error) {
	return stackitem.NewStruct([]stackitem.Item{stackitem.NewBigInteger(&s.Balance)}), nil
}

// FromStackItem implements stackitem.Convertible.
func (s *NEP17Balance) FromStackItem(item stackitem.Item) error {
	items, ok := item.Value().([]stackitem.Item)
	if !ok {
		return errors.New("not a struct")
	}
	if len(items) < 1 {
		return errors.New("no balance value")
	}
	balance, err := items[0].TryInteger()
	if err != nil {
		return fmt.Errorf("invalid balance: %w", err)
	}
	s.Balance = *balance
	return nil
}

// NEOBalanceFromBytes converts the serialized NEOBalance to a structure.
func NEOBalanceFromBytes(b []byte) (*NEOBalance, error) {
	balance := new(NEOBalance)
	err := balanceFromBytes(b, balance)
	if err != nil {
		return nil, err
	}
	return balance, nil
}

// Bytes returns a serialized NEOBalance.
func (s *NEOBalance) Bytes(sc *stackitem.SerializationContext) []byte {
	item, _ := s.ToStackItem() // Never returns an error.
	data, err := sc.Serialize(item, false)
	if err != nil {
		panic(err)
	}
	return data
}

// ToStackItem implements stackitem.Convertible interface. It never returns an error.
func (s *NEOBalance) ToStackItem() (stackitem.Item, error) {
	var voteItem stackitem.Item

	if s.VoteTo != nil {
		voteItem = stackitem.NewByteArray(s.VoteTo.Bytes())
	} else {
		voteItem = stackitem.Null{}
	}
	return stackitem.NewStruct([]stackitem.Item{
		stackitem.NewBigInteger(&s.Balance),
		stackitem.NewBigInteger(big.NewInt(int64(s.BalanceHeight))),
		voteItem,
		stackitem.NewBigInteger(&s.LastGasPerVote),
	}), nil
}

// FromStackItem converts stackitem.Item to NEOBalance.
func (s *NEOBalance) FromStackItem(item stackitem.Item) error {
	structItem, ok := item.Value().([]stackitem.Item)
	if !ok || len(structItem) < 3 {
		return errors.New("invalid stackitem length")
	}
	balance, err := structItem[0].TryInteger()
	if err != nil {
		return fmt.Errorf("invalid balance stackitem: %w", err)
	}
	s.Balance = *balance
	h, err := structItem[1].TryInteger()
	if err != nil {
		return fmt.Errorf("invalid heigh stackitem")
	}
	s.BalanceHeight = uint32(h.Int64())
	if _, ok := structItem[2].(stackitem.Null); ok {
		s.VoteTo = nil
	} else {
		bs, err := structItem[2].TryBytes()
		if err != nil {
			return fmt.Errorf("invalid public key stackitem: %w", err)
		}
		pub, err := keys.NewPublicKeyFromBytes(bs, elliptic.P256())
		if err != nil {
			return fmt.Errorf("invalid public key bytes: %w", err)
		}
		s.VoteTo = pub
	}
	if len(structItem) >= 4 {
		lastGasPerVote, err := structItem[3].TryInteger()
		if err != nil {
			return fmt.Errorf("invalid last vote reward per neo stackitem: %w", err)
		}
		s.LastGasPerVote = *lastGasPerVote
	}
	return nil
}