package morph

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

	"github.com/nspcc-dev/neo-go/pkg/core/native"
	"github.com/nspcc-dev/neo-go/pkg/core/state"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/io"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17"
	"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
	"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/emit"
	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
)

// initialAlphabetNEOAmount represents the total amount of GAS distributed between alphabet nodes.
const (
	initialAlphabetNEOAmount = native.NEOTotalSupply
	registerBatchSize        = transaction.MaxAttributes - 1
)

func (c *initializeContext) registerCandidateRange(start, end int) error {
	regPrice, err := c.getCandidateRegisterPrice()
	if err != nil {
		return fmt.Errorf("can't fetch registration price: %w", err)
	}

	w := io.NewBufBinWriter()
	emit.AppCall(w.BinWriter, neo.Hash, "setRegisterPrice", callflag.States, 1)
	for _, acc := range c.Accounts[start:end] {
		emit.AppCall(w.BinWriter, neo.Hash, "registerCandidate", callflag.States, acc.PrivateKey().PublicKey().Bytes())
		emit.Opcodes(w.BinWriter, opcode.ASSERT)
	}
	emit.AppCall(w.BinWriter, neo.Hash, "setRegisterPrice", callflag.States, regPrice)
	if w.Err != nil {
		panic(fmt.Sprintf("BUG: %v", w.Err))
	}

	signers := []actor.SignerAccount{{
		Signer:  c.getSigner(false, c.CommitteeAcc),
		Account: c.CommitteeAcc,
	}}
	for _, acc := range c.Accounts[start:end] {
		signers = append(signers, actor.SignerAccount{
			Signer: transaction.Signer{
				Account:          acc.Contract.ScriptHash(),
				Scopes:           transaction.CustomContracts,
				AllowedContracts: []util.Uint160{neo.Hash},
			},
			Account: acc,
		})
	}

	act, err := actor.New(c.Client, signers)
	if err != nil {
		return fmt.Errorf("can't create actor: %w", err)
	}
	tx, err := act.MakeRun(w.Bytes())
	if err != nil {
		return fmt.Errorf("can't create tx: %w", err)
	}
	if err := c.multiSign(tx, committeeAccountName); err != nil {
		return fmt.Errorf("can't sign a transaction: %w", err)
	}

	network := c.CommitteeAct.GetNetwork()
	for _, acc := range c.Accounts[start:end] {
		if err := acc.SignTx(network, tx); err != nil {
			return fmt.Errorf("can't sign a transaction: %w", err)
		}
	}

	return c.sendTx(tx, c.Command, true)
}

func (c *initializeContext) registerCandidates() error {
	cc, err := unwrap.Array(c.ReadOnlyInvoker.Call(neo.Hash, "getCandidates"))
	if err != nil {
		return fmt.Errorf("`getCandidates`: %w", err)
	}

	need := len(c.Accounts)
	have := len(cc)

	if need == have {
		c.Command.Println("Candidates are already registered.")
		return nil
	}

	// Register candidates in batches in order to overcome the signers amount limit.
	// See: https://github.com/nspcc-dev/neo-go/blob/master/pkg/core/transaction/transaction.go#L27
	for i := 0; i < need; i += registerBatchSize {
		start, end := i, i+registerBatchSize
		if end > need {
			end = need
		}
		// This check is sound because transactions are accepted/rejected atomically.
		if have >= end {
			continue
		}
		if err := c.registerCandidateRange(start, end); err != nil {
			return fmt.Errorf("registering candidates %d..%d: %q", start, end-1, err)
		}
	}

	return nil
}

func (c *initializeContext) transferNEOToAlphabetContracts() error {
	neoHash := neo.Hash

	ok, err := c.transferNEOFinished(neoHash)
	if ok || err != nil {
		return err
	}

	cs := c.getContract(alphabetContract)
	amount := initialAlphabetNEOAmount / len(c.Wallets)

	bw := io.NewBufBinWriter()
	for _, acc := range c.Accounts {
		h := state.CreateContractHash(acc.Contract.ScriptHash(), cs.NEF.Checksum, cs.Manifest.Name)
		emit.AppCall(bw.BinWriter, neoHash, "transfer", callflag.All,
			c.CommitteeAcc.Contract.ScriptHash(), h, int64(amount), nil)
		emit.Opcodes(bw.BinWriter, opcode.ASSERT)
	}

	if err := c.sendCommitteeTx(bw.Bytes(), false); err != nil {
		return err
	}

	return c.awaitTx()
}

func (c *initializeContext) transferNEOFinished(neoHash util.Uint160) (bool, error) {
	r := nep17.NewReader(c.ReadOnlyInvoker, neoHash)
	bal, err := r.BalanceOf(c.CommitteeAcc.Contract.ScriptHash())
	return bal.Cmp(big.NewInt(native.NEOTotalSupply)) == -1, err
}

var errGetPriceInvalid = errors.New("`getRegisterPrice`: invalid response")

func (c *initializeContext) getCandidateRegisterPrice() (int64, error) {
	switch c.Client.(type) {
	case *rpcclient.Client:
		inv := invoker.New(c.Client, nil)
		reader := neo.NewReader(inv)
		return reader.GetRegisterPrice()
	default:
		neoHash := neo.Hash
		res, err := invokeFunction(c.Client, neoHash, "getRegisterPrice", nil, nil)
		if err != nil {
			return 0, err
		}
		if len(res.Stack) == 0 {
			return 0, errGetPriceInvalid
		}
		bi, err := res.Stack[0].TryInteger()
		if err != nil || !bi.IsInt64() {
			return 0, errGetPriceInvalid
		}
		return bi.Int64(), nil
	}
}