package invoker

import (
	"errors"
	"fmt"

	"github.com/google/uuid"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)

// DefaultIteratorResultItems is the default number of results to
// request from the iterator. Typically it's the same as server's
// MaxIteratorResultItems, but different servers can have different
// settings.
const DefaultIteratorResultItems = 100

// RPCSessions is a set of RPC methods needed to retrieve values from the
// session-based iterators.
type RPCSessions interface {
	TerminateSession(sessionID uuid.UUID) (bool, error)
	TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error)
}

// RPCInvoke is a set of RPC methods needed to execute things at the current
// blockchain height.
type RPCInvoke interface {
	RPCSessions

	InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error)
	InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error)
	InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error)
}

// RPCInvokeHistoric is a set of RPC methods needed to execute things at some
// fixed point in blockchain's life.
type RPCInvokeHistoric interface {
	RPCSessions

	InvokeContractVerifyAtBlock(blockHash util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error)
	InvokeContractVerifyAtHeight(height uint32, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error)
	InvokeContractVerifyWithState(stateroot util.Uint256, contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error)
	InvokeFunctionAtBlock(blockHash util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error)
	InvokeFunctionAtHeight(height uint32, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error)
	InvokeFunctionWithState(stateroot util.Uint256, contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error)
	InvokeScriptAtBlock(blockHash util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error)
	InvokeScriptAtHeight(height uint32, script []byte, signers []transaction.Signer) (*result.Invoke, error)
	InvokeScriptWithState(stateroot util.Uint256, script []byte, signers []transaction.Signer) (*result.Invoke, error)
}

// Invoker allows to test-execute things using RPC client. Its API simplifies
// reusing the same signers list for a series of invocations and at the
// same time uses regular Go types for call parameters. It doesn't do anything with
// the result of invocation, that's left for upper (contract) layer to deal with.
// Invoker does not produce any transactions and does not change the state of the
// chain.
type Invoker struct {
	client  RPCInvoke
	signers []transaction.Signer
}

type historicConverter struct {
	client RPCInvokeHistoric
	block  *util.Uint256
	height *uint32
	root   *util.Uint256
}

// New creates an Invoker to test-execute things at the current blockchain height.
func New(client RPCInvoke, signers []transaction.Signer) *Invoker {
	return &Invoker{client, signers}
}

// NewHistoricAtBlock creates an Invoker to test-execute things at some given block.
func NewHistoricAtBlock(block util.Uint256, client RPCInvokeHistoric, signers []transaction.Signer) *Invoker {
	return New(&historicConverter{
		client: client,
		block:  &block,
	}, signers)
}

// NewHistoricAtHeight creates an Invoker to test-execute things at some given height.
func NewHistoricAtHeight(height uint32, client RPCInvokeHistoric, signers []transaction.Signer) *Invoker {
	return New(&historicConverter{
		client: client,
		height: &height,
	}, signers)
}

// NewHistoricWithState creates an Invoker to test-execute things with some given state.
func NewHistoricWithState(root util.Uint256, client RPCInvokeHistoric, signers []transaction.Signer) *Invoker {
	return New(&historicConverter{
		client: client,
		root:   &root,
	}, signers)
}

func (h *historicConverter) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
	if h.block != nil {
		return h.client.InvokeScriptAtBlock(*h.block, script, signers)
	}
	if h.height != nil {
		return h.client.InvokeScriptAtHeight(*h.height, script, signers)
	}
	if h.root != nil {
		return h.client.InvokeScriptWithState(*h.root, script, signers)
	}
	panic("uninitialized historicConverter")
}

func (h *historicConverter) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
	if h.block != nil {
		return h.client.InvokeFunctionAtBlock(*h.block, contract, operation, params, signers)
	}
	if h.height != nil {
		return h.client.InvokeFunctionAtHeight(*h.height, contract, operation, params, signers)
	}
	if h.root != nil {
		return h.client.InvokeFunctionWithState(*h.root, contract, operation, params, signers)
	}
	panic("uninitialized historicConverter")
}

func (h *historicConverter) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
	if h.block != nil {
		return h.client.InvokeContractVerifyAtBlock(*h.block, contract, params, signers, witnesses...)
	}
	if h.height != nil {
		return h.client.InvokeContractVerifyAtHeight(*h.height, contract, params, signers, witnesses...)
	}
	if h.root != nil {
		return h.client.InvokeContractVerifyWithState(*h.root, contract, params, signers, witnesses...)
	}
	panic("uninitialized historicConverter")
}

func (h *historicConverter) TerminateSession(sessionID uuid.UUID) (bool, error) {
	return h.client.TerminateSession(sessionID)
}

func (h *historicConverter) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
	return h.client.TraverseIterator(sessionID, iteratorID, maxItemsCount)
}

// Call invokes a method of the contract with the given parameters (and
// Invoker-specific list of signers) and returns the result as is.
func (v *Invoker) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) {
	ps, err := smartcontract.NewParametersFromValues(params...)
	if err != nil {
		return nil, err
	}
	return v.client.InvokeFunction(contract, operation, ps, v.signers)
}

// CallAndExpandIterator creates a script containing a call of the specified method
// of a contract with given parameters (similar to how Call operates). But then this
// script contains additional code that expects that the result of the first call is
// an iterator. This iterator is traversed extracting values from it and adding them
// into an array until maxItems is reached or iterator has no more elements. The
// result of the whole script is an array containing up to maxResultItems elements
// from the iterator returned from the contract's method call. This script is executed
// using regular JSON-API (according to the way Iterator is set up).
func (v *Invoker) CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) {
	bytes, err := smartcontract.CreateCallAndUnwrapIteratorScript(contract, method, maxItems, params...)
	if err != nil {
		return nil, fmt.Errorf("iterator unwrapper script: %w", err)
	}
	return v.Run(bytes)
}

// Verify invokes contract's verify method in the verification context with
// Invoker-specific signers and given witnesses and parameters.
func (v *Invoker) Verify(contract util.Uint160, witnesses []transaction.Witness, params ...interface{}) (*result.Invoke, error) {
	ps, err := smartcontract.NewParametersFromValues(params...)
	if err != nil {
		return nil, err
	}
	return v.client.InvokeContractVerify(contract, ps, v.signers, witnesses...)
}

// Run executes given bytecode with Invoker-specific list of signers.
func (v *Invoker) Run(script []byte) (*result.Invoke, error) {
	return v.client.InvokeScript(script, v.signers)
}

// TerminateSession closes the given session, returning an error if anything
// goes wrong.
func (v *Invoker) TerminateSession(sessionID uuid.UUID) error {
	return termSession(v.client, sessionID)
}

func termSession(rpc RPCSessions, sessionID uuid.UUID) error {
	r, err := rpc.TerminateSession(sessionID)
	if err != nil {
		return err
	}
	if !r {
		return errors.New("terminatesession returned false")
	}
	return nil
}

// TraverseIterator allows to retrieve the next batch of items from the given
// iterator in the given session (previously returned from Call or Run). It works
// both with session-backed iterators and expanded ones (which one you have
// depends on the RPC server). It can change the state of the iterator in the
// process. If num <= 0 then DefaultIteratorResultItems number of elements is
// requested. If result contains no elements, then either Iterator has no
// elements or session was expired and terminated by the server.
func (v *Invoker) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) {
	return iterateNext(v.client, sessionID, iterator, num)
}

func iterateNext(rpc RPCSessions, sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) {
	if num <= 0 {
		num = DefaultIteratorResultItems
	}

	if iterator.ID != nil {
		return rpc.TraverseIterator(sessionID, *iterator.ID, num)
	}
	if num > len(iterator.Values) {
		num = len(iterator.Values)
	}
	items := iterator.Values[:num]
	iterator.Values = iterator.Values[num:]

	return items, nil
}