package client

import (
	"crypto/elliptic"

	"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
	"github.com/nspcc-dev/neo-go/pkg/crypto/hash"
	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
	"github.com/nspcc-dev/neo-go/pkg/encoding/fixedn"
	sc "github.com/nspcc-dev/neo-go/pkg/smartcontract"
	"github.com/nspcc-dev/neo-go/pkg/util"
	"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
	"github.com/nspcc-dev/neo-go/pkg/wallet"
	"github.com/pkg/errors"
	"go.uber.org/zap"
)

type (
	notary struct {
		// extra fee to check witness of proxy contract
		// neo-go does not have an option to calculate it exactly right now
		extraVerifyFee int64

		txValidTime  uint32 // minimum amount of blocks when mainTx will be valid
		roundTime    uint32 // extra amount of blocks to synchronize sidechain height diff of inner ring nodes
		fallbackTime uint32 // amount of blocks before fallbackTx will be sent

		notary util.Uint160
		proxy  util.Uint160
		netmap util.Uint160
	}

	notaryCfg struct {
		extraVerifyFee int64

		txValidTime, roundTime, fallbackTime uint32
	}

	NotaryOption func(*notaryCfg)
)

const (
	defaultNotaryExtraFee     = 1000_0000
	defaultNotaryValidTime    = 50
	defaultNotaryRoundTime    = 100
	defaultNotaryFallbackTime = 40

	innerRingListMethod   = "innerRingList"
	notaryBalanceOfMethod = "balanceOf"

	notaryBalanceErrMsg = "can't fetch notary balance"
)

var (
	errNotaryNotEnabled = errors.New("notary support was not enabled on this client")
	errInvalidIR        = errors.New("invalid inner ring list from netmap contract")
	errUnexpectedItems  = errors.New("invalid number of NEO VM arguments on stack")
)

func defaultNotaryConfig() *notaryCfg {
	return &notaryCfg{
		extraVerifyFee: defaultNotaryExtraFee,
		txValidTime:    defaultNotaryValidTime,
		roundTime:      defaultNotaryRoundTime,
		fallbackTime:   defaultNotaryFallbackTime,
	}
}

// EnableNotarySupport creates notary structure in client that provides
// ability for client to get inner ring list from netmap contract and
// use proxy contract script hash to create tx for notary contract.
func (c *Client) EnableNotarySupport(proxy, netmap util.Uint160, opts ...NotaryOption) error {
	cfg := defaultNotaryConfig()

	for _, opt := range opts {
		opt(cfg)
	}

	notaryContract, err := c.client.GetNativeContractHash(nativenames.Notary)
	if err != nil {
		return errors.Wrap(err, "can't get notary contract script hash")
	}

	c.notary = &notary{
		notary:         notaryContract,
		proxy:          proxy,
		netmap:         netmap,
		extraVerifyFee: cfg.extraVerifyFee,
		txValidTime:    cfg.txValidTime,
		roundTime:      cfg.roundTime,
		fallbackTime:   cfg.fallbackTime,
	}

	return nil
}

// DepositNotary calls notary deposit method. Deposit is required to operate
// with notary contract. It used by notary contract in to produce fallback tx
// if main tx failed to create. Deposit isn't last forever, so it should
// be called periodically. Notary support should be enabled in client to
// use this function.
func (c *Client) DepositNotary(amount fixedn.Fixed8, delta uint32) error {
	if c.notary == nil {
		return errNotaryNotEnabled
	}

	bc, err := c.client.GetBlockCount()
	if err != nil {
		return errors.Wrap(err, "can't get blockchain height")
	}

	txHash, err := c.client.TransferNEP17(
		c.acc,
		c.notary.notary,
		c.gas,
		int64(amount),
		0,
		[]interface{}{c.acc.PrivateKey().GetScriptHash(), int64(bc + delta)},
	)
	if err != nil {
		return errors.Wrap(err, "can't make notary deposit")
	}

	c.logger.Debug("notary deposit invoke",
		zap.Int64("amount", int64(amount)),
		zap.Uint32("expire_at", bc+delta),
		zap.Stringer("tx_hash", txHash))

	return nil
}

// GetNotaryDeposit returns deposit of client's account in notary contract.
// Notary support should be enabled in client to use this function.
func (c *Client) GetNotaryDeposit() (int64, error) {
	if c.notary == nil {
		return 0, errNotaryNotEnabled
	}

	sh := c.acc.PrivateKey().PublicKey().GetScriptHash()

	items, err := c.TestInvoke(c.notary.notary, notaryBalanceOfMethod, sh)
	if err != nil {
		return 0, errors.Wrap(err, notaryBalanceErrMsg)
	}

	if len(items) != 1 {
		return 0, errors.Wrap(errUnexpectedItems, notaryBalanceErrMsg)
	}

	bigIntDeposit, err := items[0].TryInteger()
	if err != nil {
		return 0, errors.Wrap(err, notaryBalanceErrMsg)
	}

	return bigIntDeposit.Int64(), nil
}

// Invoke invokes contract method by sending tx to notary contract in
// blockchain. Fallback tx is a `RET`. Notary support should be enabled
// in client to use this function.
//
// Supported args types: int64, string, util.Uint160, []byte and bool.
func (c *Client) NotaryInvoke(contract util.Uint160, method string, args ...interface{}) error {
	if c.notary == nil {
		return errNotaryNotEnabled
	}

	// prepare arguments for test invocation

	irList, err := c.notaryInnerRingList()
	if err != nil {
		return err
	}

	_, n := mn(irList)
	u8n := uint8(n)

	cosigners, err := c.notaryCosigners(irList)
	if err != nil {
		return err
	}

	params, err := invocationParams(args...)
	if err != nil {
		return err
	}

	// make test invocation of the method
	test, err := c.client.InvokeFunction(contract, method, params, cosigners)
	if err != nil {
		return err
	}

	// if test invocation failed, then return error
	if len(test.Script) == 0 {
		return errEmptyInvocationScript
	}

	// after test invocation we build main multisig transaction

	multiaddrAccount, err := c.notaryMultisigAccount(irList)
	if err != nil {
		return err
	}

	until, err := c.notaryTxValidationLimit()
	if err != nil {
		return err
	}

	// prepare main tx
	mainTx := &transaction.Transaction{
		Nonce:           1,
		SystemFee:       test.GasConsumed,
		ValidUntilBlock: until,
		Script:          test.Script,
		Attributes: []transaction.Attribute{
			{
				Type:  transaction.NotaryAssistedT,
				Value: &transaction.NotaryAssisted{NKeys: u8n},
			},
		},
		Signers: cosigners,
		Network: c.client.GetNetwork(),
	}

	// calculate notary fee
	notaryFee, err := c.client.CalculateNotaryFee(u8n)
	if err != nil {
		return err
	}

	// add network fee for cosigners
	err = c.client.AddNetworkFee(
		mainTx,
		notaryFee+c.notary.extraVerifyFee,
		c.notaryAccounts(multiaddrAccount)...,
	)
	if err != nil {
		return err
	}

	// define witnesses
	mainTx.Scripts = c.notaryWitnesses(multiaddrAccount, mainTx)

	resp, err := c.client.SignAndPushP2PNotaryRequest(mainTx,
		[]byte{byte(opcode.RET)},
		-1,
		0,
		c.notary.fallbackTime,
		c.acc)
	if err != nil {
		return err
	}

	c.logger.Debug("notary request invoked",
		zap.String("method", method),
		zap.Stringer("tx_hash", resp.Hash().Reverse()))

	return nil
}

func (c *Client) notaryCosigners(ir []*keys.PublicKey) ([]transaction.Signer, error) {
	s := make([]transaction.Signer, 0, 3)

	// first we have proxy contract signature, as it will pay for the execution
	s = append(s, transaction.Signer{
		Account: c.notary.proxy,
		Scopes:  transaction.None,
	})

	// then we have inner ring multiaddress signature
	m, _ := mn(ir)

	multisigScript, err := sc.CreateMultiSigRedeemScript(m, ir)
	if err != nil {
		return nil, errors.Wrap(err, "can't create ir multisig redeem script")
	}

	s = append(s, transaction.Signer{
		Account: hash.Hash160(multisigScript),
		Scopes:  transaction.Global,
	})

	// last one is a placeholder for notary contract signature
	s = append(s, transaction.Signer{
		Account: c.notary.notary,
		Scopes:  transaction.None,
	})

	return s, nil
}

func (c *Client) notaryAccounts(multiaddr *wallet.Account) []*wallet.Account {
	if multiaddr == nil {
		return nil
	}

	a := make([]*wallet.Account, 0, 3)

	// first we have proxy account, as it will pay for the execution
	a = append(a, &wallet.Account{
		Contract: &wallet.Contract{
			Deployed: true,
		},
	})

	// then we have inner ring multiaddress account
	a = append(a, multiaddr)

	// last one is a placeholder for notary contract account
	a = append(a, &wallet.Account{
		Contract: &wallet.Contract{},
	})

	return a
}

func (c *Client) notaryWitnesses(multiaddr *wallet.Account, tx *transaction.Transaction) []transaction.Witness {
	if multiaddr == nil || tx == nil {
		return nil
	}

	w := make([]transaction.Witness, 0, 3)

	// first we have empty proxy witness, because notary will execute `Verify`
	// method on the proxy contract to check witness
	w = append(w, transaction.Witness{
		InvocationScript:   []byte{},
		VerificationScript: []byte{},
	})

	// then we have inner ring multiaddress witness
	w = append(w, transaction.Witness{
		InvocationScript: append(
			[]byte{byte(opcode.PUSHDATA1), 64},
			multiaddr.PrivateKey().Sign(tx.GetSignedPart())...,
		),
		VerificationScript: multiaddr.GetVerificationScript(),
	})

	// last one is a placeholder for notary contract witness
	w = append(w, transaction.Witness{
		InvocationScript: append(
			[]byte{byte(opcode.PUSHDATA1), 64},
			make([]byte, 64)...,
		),
		VerificationScript: []byte{},
	})

	return w
}

func (c *Client) notaryInnerRingList() ([]*keys.PublicKey, error) {
	data, err := c.TestInvoke(c.notary.netmap, innerRingListMethod)
	if err != nil {
		return nil, errors.Wrap(err, "test invoke error")
	}

	if len(data) == 0 {
		return nil, errors.Wrap(errInvalidIR, "test invoke returned empty stack")
	}

	prms, err := ArrayFromStackItem(data[0])
	if err != nil {
		return nil, errors.Wrap(err, "test invoke returned non array element")
	}

	res := make([]*keys.PublicKey, 0, len(prms))
	for i := range prms {
		nodePrms, err := ArrayFromStackItem(prms[i])
		if err != nil {
			return nil, errors.Wrap(err, "inner ring node structure is not an array")
		}

		if len(nodePrms) == 0 {
			return nil, errors.Wrap(errInvalidIR, "inner ring node structure is empty array")
		}

		rawKey, err := BytesFromStackItem(nodePrms[0])
		if err != nil {
			return nil, errors.Wrap(err, "inner ring public key is not slice of bytes")
		}

		key, err := keys.NewPublicKeyFromBytes(rawKey, elliptic.P256())
		if err != nil {
			return nil, errors.Wrap(err, "can't parse inner ring public key bytes")
		}

		res = append(res, key)
	}

	return res, nil
}

func (c *Client) notaryMultisigAccount(ir []*keys.PublicKey) (*wallet.Account, error) {
	m, _ := mn(ir)

	multisigAccount := wallet.NewAccountFromPrivateKey(c.acc.PrivateKey())

	err := multisigAccount.ConvertMultisig(m, ir)
	if err != nil {
		return nil, errors.Wrap(err, "can't make inner ring multisig wallet")
	}

	return multisigAccount, nil
}

func (c *Client) notaryTxValidationLimit() (uint32, error) {
	bc, err := c.client.GetBlockCount()
	if err != nil {
		return 0, errors.Wrap(err, "can't get current blockchain height")
	}

	min := bc + c.notary.txValidTime
	rounded := (min/c.notary.roundTime + 1) * c.notary.roundTime

	return rounded, nil
}

func invocationParams(args ...interface{}) ([]sc.Parameter, error) {
	params := make([]sc.Parameter, 0, len(args))

	for i := range args {
		param, err := toStackParameter(args[i])
		if err != nil {
			return nil, err
		}

		params = append(params, param)
	}

	return params, nil
}

// mn returns M and N multi signature numbers. For NeoFS N is a length of
// inner ring list, and M is a 2/3+1 of it (like in dBFT).
func mn(ir []*keys.PublicKey) (m int, n int) {
	n = len(ir)
	m = n*2/3 + 1

	return
}

// WithExtraVerifyFee returns a notary support option for client
// that specifies extra fee to check witness of proxy contract.
func WithExtraVerifyFee(fee int64) NotaryOption {
	return func(c *notaryCfg) {
		c.extraVerifyFee = fee
	}
}

// WithTxValidTime returns a notary support option for client
// that specifies minimum amount of blocks when mainTx will be valid.
func WithTxValidTime(t uint32) NotaryOption {
	return func(c *notaryCfg) {
		c.txValidTime = t
	}
}

// WithRoundTime returns a notary support option for client
// that specifies extra blocks to synchronize side chain
// height diff of inner ring nodes.
func WithRoundTime(t uint32) NotaryOption {
	return func(c *notaryCfg) {
		c.roundTime = t
	}
}

// WithFallbackTime returns a notary support option for client
// that specifies amount of blocks before fallbackTx will be sent.
// Should be less than TxValidTime.
func WithFallbackTime(t uint32) NotaryOption {
	return func(c *notaryCfg) {
		c.fallbackTime = t
	}
}