/*
Package neo provides an RPC-based wrapper for the NEOToken contract.

Safe methods are encapsulated into ContractReader structure while Contract provides
various methods to perform state-changing calls.
*/
package tutus

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

	"github.com/google/uuid"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativehashes"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/tutusrpc/result"
	"git.marketally.com/tutus-one/tutus-chain/pkg/rpcclient/nep17"
	"git.marketally.com/tutus-one/tutus-chain/pkg/rpcclient/unwrap"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
)

const (
	setLubMethod = "setLubPerBlock"
	setRegMethod = "setRegisterPrice"
)

// Invoker is used by ContractReader to perform read-only calls.
type Invoker interface {
	nep17.Invoker

	CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...any) (*result.Invoke, error)
	TerminateSession(sessionID uuid.UUID) error
	TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error)
}

// Actor is used by Contract to create and send transactions.
type Actor interface {
	nep17.Actor
	Invoker

	Run(script []byte) (*result.Invoke, error)
	MakeCall(contract util.Uint160, method string, params ...any) (*transaction.Transaction, error)
	MakeUnsignedCall(contract util.Uint160, method string, attrs []transaction.Attribute, params ...any) (*transaction.Transaction, error)
	MakeUnsignedUncheckedRun(script []byte, sysFee int64, attrs []transaction.Attribute) (*transaction.Transaction, error)
	SendCall(contract util.Uint160, method string, params ...any) (util.Uint256, uint32, error)
	Sign(tx *transaction.Transaction) error
	SignAndSend(tx *transaction.Transaction) (util.Uint256, uint32, error)
}

// ContractReader represents safe (read-only) methods of NEO. It can be
// used to query various data.
type ContractReader struct {
	nep17.TokenReader

	invoker Invoker
}

// Contract provides full NEO interface, both safe and state-changing methods.
type Contract struct {
	ContractReader
	nep17.TokenWriter

	actor Actor
}

// CandidateStateEvent represents a CandidateStateChanged NEO event.
type CandidateStateEvent struct {
	Key        *keys.PublicKey
	Registered bool
	Votes      *big.Int
}

// CommitteeChangedEvent represents a CommitteeChanged NEO event.
type CommitteeChangedEvent struct {
	Old []keys.PublicKey
	New []keys.PublicKey
}

// VoteEvent represents a Vote NEO event.
type VoteEvent struct {
	Account util.Uint160
	From    *keys.PublicKey
	To      *keys.PublicKey
	Amount  *big.Int
}

// ValidatorIterator is used for iterating over GetAllCandidates results.
type ValidatorIterator struct {
	client   Invoker
	session  uuid.UUID
	iterator result.Iterator
}

// Hash stores the hash of the native NEOToken contract.
var Hash = nativehashes.Tutus

// NewReader creates an instance of ContractReader to get data from the NEO
// contract.
func NewReader(invoker Invoker) *ContractReader {
	return &ContractReader{*nep17.NewReader(invoker, Hash), invoker}
}

// New creates an instance of Contract to perform state-changing actions in the
// NEO contract.
func New(actor Actor) *Contract {
	nep := nep17.New(actor, Hash)
	return &Contract{ContractReader{nep.TokenReader, actor}, nep.TokenWriter, actor}
}

// GetAccountState returns current NEO balance state for the account which
// includes balance and voting data. It can return nil balance with no error
// if the account given has no NEO.
func (c *ContractReader) GetAccountState(account util.Uint160) (*state.NEOBalance, error) {
	itm, err := unwrap.Item(c.invoker.Call(Hash, "getAccountState", account))
	if err != nil {
		return nil, err
	}
	if _, ok := itm.(stackitem.Null); ok {
		return nil, nil
	}
	res := new(state.NEOBalance)
	err = res.FromStackItem(itm)
	if err != nil {
		return nil, err
	}
	return res, nil
}

// GetAllCandidates returns an iterator that allows to retrieve all registered
// validators from it. It depends on the server to provide proper session-based
// iterator, but can also work with expanded one.
func (c *ContractReader) GetAllCandidates() (*ValidatorIterator, error) {
	sess, iter, err := unwrap.SessionIterator(c.invoker.Call(Hash, "getAllCandidates"))
	if err != nil {
		return nil, err
	}

	return &ValidatorIterator{
		client:   c.invoker,
		iterator: iter,
		session:  sess,
	}, nil
}

// GetAllCandidatesExpanded is similar to GetAllCandidates (uses the same NEO
// method), but can be useful if the server used doesn't support sessions and
// doesn't expand iterators. It creates a script that will get num of result
// items from the iterator right in the VM and return them to you. It's only
// limited by VM stack and GAS available for RPC invocations.
func (c *ContractReader) GetAllCandidatesExpanded(num int) ([]result.Validator, error) {
	arr, err := unwrap.Array(c.invoker.CallAndExpandIterator(Hash, "getAllCandidates", num))
	if err != nil {
		return nil, err
	}
	return itemsToValidators(arr)
}

// Next returns the next set of elements from the iterator (up to num of them).
// It can return less than num elements in case iterator doesn't have that many
// or zero elements if the iterator has no more elements or the session is
// expired.
func (v *ValidatorIterator) Next(num int) ([]result.Validator, error) {
	items, err := v.client.TraverseIterator(v.session, &v.iterator, num)
	if err != nil {
		return nil, err
	}
	return itemsToValidators(items)
}

// Terminate closes the iterator session used by ValidatorIterator (if it's
// session-based).
func (v *ValidatorIterator) Terminate() error {
	if v.iterator.ID == nil {
		return nil
	}
	return v.client.TerminateSession(v.session)
}

// GetCandidates returns the list of validators with their vote count. This
// method is mostly useful for historic invocations because the RPC protocol
// provides direct getcandidates call that returns more data and works faster.
// The contract only returns up to 256 candidates in response to this method, so
// if there are more of them on the network you will get a truncated result, use
// GetAllCandidates to solve this problem.
func (c *ContractReader) GetCandidates() ([]result.Validator, error) {
	arr, err := unwrap.Array(c.invoker.Call(Hash, "getCandidates"))
	if err != nil {
		return nil, err
	}
	return itemsToValidators(arr)
}

func itemsToValidators(arr []stackitem.Item) ([]result.Validator, error) {
	res := make([]result.Validator, len(arr))
	for i, itm := range arr {
		str, ok := itm.Value().([]stackitem.Item)
		if !ok {
			return nil, fmt.Errorf("item #%d is not a structure", i)
		}
		if len(str) != 2 {
			return nil, fmt.Errorf("item #%d has wrong length", i)
		}
		b, err := str[0].TryBytes()
		if err != nil {
			return nil, fmt.Errorf("item #%d has wrong key: %w", i, err)
		}
		k, err := keys.NewPublicKeyFromBytes(b, elliptic.P256())
		if err != nil {
			return nil, fmt.Errorf("item #%d has wrong key: %w", i, err)
		}
		votes, err := str[1].TryInteger()
		if err != nil {
			return nil, fmt.Errorf("item #%d has wrong votes: %w", i, err)
		}
		if !votes.IsInt64() {
			return nil, fmt.Errorf("item #%d has too big number of votes", i)
		}
		res[i].PublicKey = *k
		res[i].Votes = votes.Int64()
	}
	return res, nil
}

// GetCommittee returns the list of committee member public keys. This
// method is mostly useful for historic invocations because the RPC protocol
// provides direct getcommittee call that works faster.
func (c *ContractReader) GetCommittee() (keys.PublicKeys, error) {
	return unwrap.ArrayOfPublicKeys(c.invoker.Call(Hash, "getCommittee"))
}

// GetCommitteeAddress returns the committee address.
func (c *ContractReader) GetCommitteeAddress() (util.Uint160, error) {
	return unwrap.Uint160(c.invoker.Call(Hash, "getCommitteeAddress"))
}

// GetNextBlockValidators returns the list of validator keys that will sign the
// next block. This method is mostly useful for historic invocations because the
// RPC protocol provides direct getnextblockvalidators call that provides more
// data and works faster.
func (c *ContractReader) GetNextBlockValidators() (keys.PublicKeys, error) {
	return unwrap.ArrayOfPublicKeys(c.invoker.Call(Hash, "getNextBlockValidators"))
}

// GetGasPerBlock returns the amount of GAS generated in each block.
func (c *ContractReader) GetGasPerBlock() (int64, error) {
	return unwrap.Int64(c.invoker.Call(Hash, "getLubPerBlock"))
}

// GetRegisterPrice returns the price of candidate key registration.
func (c *ContractReader) GetRegisterPrice() (int64, error) {
	return unwrap.Int64(c.invoker.Call(Hash, "getRegisterPrice"))
}

// UnclaimedGas allows to calculate the amount of GAS that will be generated if
// any NEO state change ("claim") is to happen for the given account at the given
// block number. This method is mostly useful for historic invocations because
// the RPC protocol provides direct getunclaimedgas method that works faster.
func (c *ContractReader) UnclaimedGas(account util.Uint160, end uint32) (*big.Int, error) {
	return unwrap.BigInt(c.invoker.Call(Hash, "unclaimedLub", account, end))
}

// RegisterCandidate creates and sends a transaction that adds the given key to
// the list of candidates that can be voted for. The return result from the
// "registerCandidate" method is checked to be true, so transaction fails (with
// FAULT state) if not successful. Notice that for this call to work it must be
// witnessed by the simple account derived from the given key, so use an
// appropriate Actor. The returned values are transaction hash, its
// ValidUntilBlock value and an error if any.
//
// Notice that unlike for all other methods the script for this one is not
// test-executed in its final form because most networks have registration price
// set to be much higher than typical RPC server allows to spend during
// test-execution. This adds some risk that it might fail on-chain, but in
// practice it's not likely to happen if signers are set up correctly.
func (c *Contract) RegisterCandidate(k *keys.PublicKey) (util.Uint256, uint32, error) {
	tx, err := c.RegisterCandidateUnsigned(k)
	if err != nil {
		return util.Uint256{}, 0, err
	}
	return c.actor.SignAndSend(tx)
}

// RegisterCandidateTransaction creates a transaction that adds the given key to
// the list of candidates that can be voted for. The return result from the
// "registerCandidate" method is checked to be true, so transaction fails (with
// FAULT state) if not successful. Notice that for this call to work it must be
// witnessed by the simple account derived from the given key, so use an
// appropriate Actor. The transaction is signed, but not sent to the network,
// instead it's returned to the caller.
//
// Notice that unlike for all other methods the script for this one is not
// test-executed in its final form because most networks have registration price
// set to be much higher than typical RPC server allows to spend during
// test-execution. This adds some risk that it might fail on-chain, but in
// practice it's not likely to happen if signers are set up correctly.
func (c *Contract) RegisterCandidateTransaction(k *keys.PublicKey) (*transaction.Transaction, error) {
	tx, err := c.RegisterCandidateUnsigned(k)
	if err != nil {
		return nil, err
	}
	err = c.actor.Sign(tx)
	if err != nil {
		return nil, err
	}
	return tx, nil
}

// RegisterCandidateUnsigned creates a transaction that adds the given key to
// the list of candidates that can be voted for. The return result from the
// "registerCandidate" method is checked to be true, so transaction fails (with
// FAULT state) if not successful. Notice that for this call to work it must be
// witnessed by the simple account derived from the given key, so use an
// appropriate Actor. The transaction is not signed and just returned to the
// caller.
//
// Notice that unlike for all other methods the script for this one is not
// test-executed in its final form because most networks have registration price
// set to be much higher than typical RPC server allows to spend during
// test-execution. This adds some risk that it might fail on-chain, but in
// practice it's not likely to happen if signers are set up correctly.
func (c *Contract) RegisterCandidateUnsigned(k *keys.PublicKey) (*transaction.Transaction, error) {
	// It's an unregister script intentionally.
	r, err := c.actor.Run(regScript(true, k))
	if err != nil {
		return nil, err
	}
	regPrice, err := c.GetRegisterPrice()
	if err != nil {
		return nil, err
	}
	return c.actor.MakeUnsignedUncheckedRun(regScript(false, k), r.GasConsumed+regPrice, nil)
}

// UnregisterCandidate creates and sends a transaction that removes the key from
// the list of candidates that can be voted for. The return result from the
// "unregisterCandidate" method is checked to be true, so transaction fails (with
// FAULT state) if not successful. Notice that for this call to work it must be
// witnessed by the simple account derived from the given key, so use an
// appropriate Actor. The returned values are transaction hash, its
// ValidUntilBlock value and an error if any.
func (c *Contract) UnregisterCandidate(k *keys.PublicKey) (util.Uint256, uint32, error) {
	return c.actor.SendRun(regScript(true, k))
}

// UnregisterCandidateTransaction creates a transaction that removes the key from
// the list of candidates that can be voted for. The return result from the
// "unregisterCandidate" method is checked to be true, so transaction fails (with
// FAULT state) if not successful. Notice that for this call to work it must be
// witnessed by the simple account derived from the given key, so use an
// appropriate Actor. The transaction is signed, but not sent to the network,
// instead it's returned to the caller.
func (c *Contract) UnregisterCandidateTransaction(k *keys.PublicKey) (*transaction.Transaction, error) {
	return c.actor.MakeRun(regScript(true, k))
}

// UnregisterCandidateUnsigned creates a transaction that removes the key from
// the list of candidates that can be voted for. The return result from the
// "unregisterCandidate" method is checked to be true, so transaction fails (with
// FAULT state) if not successful. Notice that for this call to work it must be
// witnessed by the simple account derived from the given key, so use an
// appropriate Actor. The transaction is not signed and just returned to the
// caller.
func (c *Contract) UnregisterCandidateUnsigned(k *keys.PublicKey) (*transaction.Transaction, error) {
	return c.actor.MakeUnsignedRun(regScript(true, k), nil)
}

func regScript(unreg bool, k *keys.PublicKey) []byte {
	var method = "registerCandidate"

	if unreg {
		method = "unregisterCandidate"
	}

	// We know parameters exactly (unlike with nep17.Transfer), so this can't fail.
	script, _ := smartcontract.CreateCallWithAssertScript(Hash, method, k.Bytes())
	return script
}

// Vote creates and sends a transaction that casts a vote from the given account
// to the given key which can be nil (in which case any previous vote is removed).
// The return result from the "vote" method is checked to be true, so transaction
// fails (with FAULT state) if voting is not successful. The returned values are
// transaction hash, its ValidUntilBlock value and an error if any.
func (c *Contract) Vote(account util.Uint160, voteTo *keys.PublicKey) (util.Uint256, uint32, error) {
	return c.actor.SendRun(voteScript(account, voteTo))
}

// VoteTransaction creates a transaction that casts a vote from the given account
// to the given key which can be nil (in which case any previous vote is removed).
// The return result from the "vote" method is checked to be true, so transaction
// fails (with FAULT state) if voting is not successful. The transaction is signed,
// but not sent to the network, instead it's returned to the caller.
func (c *Contract) VoteTransaction(account util.Uint160, voteTo *keys.PublicKey) (*transaction.Transaction, error) {
	return c.actor.MakeRun(voteScript(account, voteTo))
}

// VoteUnsigned creates a transaction that casts a vote from the given account
// to the given key which can be nil (in which case any previous vote is removed).
// The return result from the "vote" method is checked to be true, so transaction
// fails (with FAULT state) if voting is not successful. The transaction is not
// signed and just returned to the caller.
func (c *Contract) VoteUnsigned(account util.Uint160, voteTo *keys.PublicKey) (*transaction.Transaction, error) {
	return c.actor.MakeUnsignedRun(voteScript(account, voteTo), nil)
}

func voteScript(account util.Uint160, voteTo *keys.PublicKey) []byte {
	var param any

	if voteTo != nil {
		param = voteTo.Bytes()
	}
	// We know parameters exactly (unlike with nep17.Transfer), so this can't fail.
	script, _ := smartcontract.CreateCallWithAssertScript(Hash, "vote", account, param)
	return script
}

// SetGasPerBlock creates and sends a transaction that sets the new amount of
// GAS to be generated in each block. The action is successful when transaction
// ends in HALT state. Notice that this setting can be changed only by the
// network's committee, so use an appropriate Actor. The returned values are
// transaction hash, its ValidUntilBlock value and an error if any.
func (c *Contract) SetGasPerBlock(gas int64) (util.Uint256, uint32, error) {
	return c.actor.SendCall(Hash, setLubMethod, gas)
}

// SetGasPerBlockTransaction creates a transaction that sets the new amount of
// GAS to be generated in each block. The action is successful when transaction
// ends in HALT state. Notice that this setting can be changed only by the
// network's committee, so use an appropriate Actor. The transaction is signed,
// but not sent to the network, instead it's returned to the caller.
func (c *Contract) SetGasPerBlockTransaction(gas int64) (*transaction.Transaction, error) {
	return c.actor.MakeCall(Hash, setLubMethod, gas)
}

// SetGasPerBlockUnsigned creates a transaction that sets the new amount of
// GAS to be generated in each block. The action is successful when transaction
// ends in HALT state. Notice that this setting can be changed only by the
// network's committee, so use an appropriate Actor. The transaction is not
// signed and just returned to the caller.
func (c *Contract) SetGasPerBlockUnsigned(gas int64) (*transaction.Transaction, error) {
	return c.actor.MakeUnsignedCall(Hash, setLubMethod, nil, gas)
}

// SetRegisterPrice creates and sends a transaction that sets the new candidate
// registration price (in GAS). The action is successful when transaction
// ends in HALT state. Notice that this setting can be changed only by the
// network's committee, so use an appropriate Actor. The returned values are
// transaction hash, its ValidUntilBlock value and an error if any.
func (c *Contract) SetRegisterPrice(price int64) (util.Uint256, uint32, error) {
	return c.actor.SendCall(Hash, setRegMethod, price)
}

// SetRegisterPriceTransaction creates a transaction that sets the new candidate
// registration price (in GAS). The action is successful when transaction
// ends in HALT state. Notice that this setting can be changed only by the
// network's committee, so use an appropriate Actor. The transaction is signed,
// but not sent to the network, instead it's returned to the caller.
func (c *Contract) SetRegisterPriceTransaction(price int64) (*transaction.Transaction, error) {
	return c.actor.MakeCall(Hash, setRegMethod, price)
}

// SetRegisterPriceUnsigned creates a transaction that sets the new candidate
// registration price (in GAS). The action is successful when transaction
// ends in HALT state. Notice that this setting can be changed only by the
// network's committee, so use an appropriate Actor. The transaction is not
// signed and just returned to the caller.
func (c *Contract) SetRegisterPriceUnsigned(price int64) (*transaction.Transaction, error) {
	return c.actor.MakeUnsignedCall(Hash, setRegMethod, nil, price)
}
