package account

import (
	"context"
	stdjson "encoding/json"

	"github.com/vapor/blockchain/signers"
	"github.com/vapor/blockchain/txbuilder"
	"github.com/vapor/common"
	"github.com/vapor/consensus"
	"github.com/vapor/crypto/ed25519/chainkd"
	"github.com/vapor/encoding/json"
	"github.com/vapor/errors"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
	"github.com/vapor/protocol/vm/vmutil"
)

var (
	//chainTxUtxoNum maximum utxo quantity in a tx
	chainTxUtxoNum = 20
	//chainTxMergeGas chain tx gas
	chainTxMergeGas = uint64(0)
)

//DecodeSpendAction unmarshal JSON-encoded data of spend action
func (m *Manager) DecodeSpendAction(data []byte) (txbuilder.Action, error) {
	a := &spendAction{accounts: m}
	return a, stdjson.Unmarshal(data, a)
}

type spendAction struct {
	accounts *Manager
	bc.AssetAmount
	AccountID      string `json:"account_id"`
	UseUnconfirmed bool   `json:"use_unconfirmed"`
}

func (a *spendAction) ActionType() string {
	return "spend_account"
}

// MergeSpendAction merge common assetID and accountID spend action
func MergeSpendAction(actions []txbuilder.Action) []txbuilder.Action {
	resultActions := []txbuilder.Action{}
	spendActionMap := make(map[string]*spendAction)

	for _, act := range actions {
		switch act := act.(type) {
		case *spendAction:
			actionKey := act.AssetId.String() + act.AccountID
			if tmpAct, ok := spendActionMap[actionKey]; ok {
				tmpAct.Amount += act.Amount
				tmpAct.UseUnconfirmed = tmpAct.UseUnconfirmed || act.UseUnconfirmed
			} else {
				spendActionMap[actionKey] = act
				resultActions = append(resultActions, act)
			}
		default:
			resultActions = append(resultActions, act)
		}
	}
	return resultActions
}

//calcMergeGas calculate the gas required that n utxos are merged into one
func calcMergeGas(num int) uint64 {
	gas := uint64(0)
	for num > 1 {
		gas += chainTxMergeGas
		num -= chainTxUtxoNum - 1
	}
	return gas
}

func (m *Manager) reserveBtmUtxoChain(builder *txbuilder.TemplateBuilder, accountID string, amount uint64, useUnconfirmed bool) ([]*UTXO, error) {
	reservedAmount := uint64(0)
	utxos := []*UTXO{}
	for gasAmount := uint64(0); reservedAmount < gasAmount+amount; gasAmount = calcMergeGas(len(utxos)) {
		reserveAmount := amount + gasAmount - reservedAmount
		res, err := m.utxoKeeper.Reserve(accountID, consensus.BTMAssetID, reserveAmount, useUnconfirmed, nil, builder.MaxTime())
		if err != nil {
			return nil, err
		}

		builder.OnRollback(func() { m.utxoKeeper.Cancel(res.id) })
		reservedAmount += reserveAmount + res.change
		utxos = append(utxos, res.utxos[:]...)
	}
	return utxos, nil
}

func (m *Manager) BuildBtmTxChain(utxos []*UTXO, signer *signers.Signer) ([]*txbuilder.Template, *UTXO, error) {
	if len(utxos) == 0 {
		return nil, nil, errors.New("mergeSpendActionUTXO utxos num 0")
	}

	tpls := []*txbuilder.Template{}
	if len(utxos) == 1 {
		return tpls, utxos[len(utxos)-1], nil
	}

	acp, err := m.GetLocalCtrlProgramByAddress(utxos[0].Address)
	if err != nil {
		return nil, nil, err
	}

	buildAmount := uint64(0)
	builder := &txbuilder.TemplateBuilder{}
	for index := 0; index < len(utxos); index++ {
		input, sigInst, err := UtxoToInputs(signer, utxos[index])
		if err != nil {
			return nil, nil, err
		}

		if err = builder.AddInput(input, sigInst); err != nil {
			return nil, nil, err
		}

		buildAmount += input.Amount()
		if builder.InputCount() != chainTxUtxoNum && index != len(utxos)-1 {
			continue
		}

		outAmount := buildAmount - chainTxMergeGas
		output := types.NewIntraChainOutput(*consensus.BTMAssetID, outAmount, acp.ControlProgram)
		if err := builder.AddOutput(output); err != nil {
			return nil, nil, err
		}

		tpl, _, err := builder.Build()
		if err != nil {
			return nil, nil, err
		}

		bcOut, err := tpl.Transaction.IntraChainOutput(*tpl.Transaction.ResultIds[0])
		if err != nil {
			return nil, nil, err
		}

		utxos = append(utxos, &UTXO{
			OutputID:            *tpl.Transaction.ResultIds[0],
			AssetID:             *consensus.BTMAssetID,
			Amount:              outAmount,
			ControlProgram:      acp.ControlProgram,
			SourceID:            *bcOut.Source.Ref,
			SourcePos:           bcOut.Source.Position,
			ControlProgramIndex: acp.KeyIndex,
			Address:             acp.Address,
			Change:              acp.Change,
		})

		tpls = append(tpls, tpl)
		buildAmount = 0
		builder = &txbuilder.TemplateBuilder{}
		if index == len(utxos)-2 {
			break
		}
	}
	return tpls, utxos[len(utxos)-1], nil
}

// SpendAccountChain build the spend action with auto merge utxo function
func SpendAccountChain(ctx context.Context, builder *txbuilder.TemplateBuilder, action txbuilder.Action) ([]*txbuilder.Template, error) {
	act, ok := action.(*spendAction)
	if !ok {
		return nil, errors.New("fail to convert the spend action")
	}

	if *act.AssetId != *consensus.BTMAssetID {
		return nil, errors.New("spend chain action only support BTM")
	}

	utxos, err := act.accounts.reserveBtmUtxoChain(builder, act.AccountID, act.Amount, act.UseUnconfirmed)
	if err != nil {
		return nil, err
	}

	acct, err := act.accounts.FindByID(act.AccountID)
	if err != nil {
		return nil, err
	}

	tpls, utxo, err := act.accounts.BuildBtmTxChain(utxos, acct.Signer)
	if err != nil {
		return nil, err
	}

	input, sigInst, err := UtxoToInputs(acct.Signer, utxo)
	if err != nil {
		return nil, err
	}

	if err := builder.AddInput(input, sigInst); err != nil {
		return nil, err
	}

	if utxo.Amount > act.Amount {
		if err = builder.AddOutput(types.NewIntraChainOutput(*consensus.BTMAssetID, utxo.Amount-act.Amount, utxo.ControlProgram)); err != nil {
			return nil, errors.Wrap(err, "adding change output")
		}
	}
	return tpls, nil
}

func (a *spendAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
	var missing []string
	if a.AccountID == "" {
		missing = append(missing, "account_id")
	}
	if a.AssetId.IsZero() {
		missing = append(missing, "asset_id")
	}
	if len(missing) > 0 {
		return txbuilder.MissingFieldsError(missing...)
	}

	acct, err := a.accounts.FindByID(a.AccountID)
	if err != nil {
		return errors.Wrap(err, "get account info")
	}

	res, err := a.accounts.utxoKeeper.Reserve(a.AccountID, a.AssetId, a.Amount, a.UseUnconfirmed, nil, b.MaxTime())
	if err != nil {
		return errors.Wrap(err, "reserving utxos")
	}

	// Cancel the reservation if the build gets rolled back.
	b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) })
	for _, r := range res.utxos {
		txInput, sigInst, err := UtxoToInputs(acct.Signer, r)
		if err != nil {
			return errors.Wrap(err, "creating inputs")
		}

		if err = b.AddInput(txInput, sigInst); err != nil {
			return errors.Wrap(err, "adding inputs")
		}
	}

	if res.change > 0 {
		acp, err := a.accounts.CreateAddress(a.AccountID, true)
		if err != nil {
			return errors.Wrap(err, "creating control program")
		}

		// Don't insert the control program until callbacks are executed.
		a.accounts.insertControlProgramDelayed(b, acp)
		if err = b.AddOutput(types.NewIntraChainOutput(*a.AssetId, res.change, acp.ControlProgram)); err != nil {
			return errors.Wrap(err, "adding change output")
		}
	}
	return nil
}

//DecodeSpendUTXOAction unmarshal JSON-encoded data of spend utxo action
func (m *Manager) DecodeSpendUTXOAction(data []byte) (txbuilder.Action, error) {
	a := &spendUTXOAction{accounts: m}
	return a, stdjson.Unmarshal(data, a)
}

type spendUTXOAction struct {
	accounts       *Manager
	OutputID       *bc.Hash                     `json:"output_id"`
	UseUnconfirmed bool                         `json:"use_unconfirmed"`
	Arguments      []txbuilder.ContractArgument `json:"arguments"`
}

func (a *spendUTXOAction) ActionType() string {
	return "spend_account_unspent_output"
}

func (a *spendUTXOAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
	if a.OutputID == nil {
		return txbuilder.MissingFieldsError("output_id")
	}

	res, err := a.accounts.utxoKeeper.ReserveParticular(*a.OutputID, a.UseUnconfirmed, b.MaxTime())
	if err != nil {
		return err
	}

	b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) })
	var accountSigner *signers.Signer
	if len(res.utxos[0].AccountID) != 0 {
		account, err := a.accounts.FindByID(res.utxos[0].AccountID)
		if err != nil {
			return err
		}

		accountSigner = account.Signer
	}

	txInput, sigInst, err := UtxoToInputs(accountSigner, res.utxos[0])
	if err != nil {
		return err
	}

	if a.Arguments == nil {
		return b.AddInput(txInput, sigInst)
	}

	sigInst = &txbuilder.SigningInstruction{}
	if err := txbuilder.AddContractArgs(sigInst, a.Arguments); err != nil {
		return err
	}

	return b.AddInput(txInput, sigInst)
}

// UtxoToInputs convert an utxo to the txinput
func UtxoToInputs(signer *signers.Signer, u *UTXO) (*types.TxInput, *txbuilder.SigningInstruction, error) {
	txInput := &types.TxInput{}
	if u.Vote == nil {
		txInput = types.NewSpendInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram)
	} else {
		txInput = types.NewVetoInput(nil, u.SourceID, u.AssetID, u.Amount, u.SourcePos, u.ControlProgram, u.Vote)
	}
	sigInst := &txbuilder.SigningInstruction{}
	if signer == nil {
		return txInput, sigInst, nil
	}

	path, err := signers.Path(signer, signers.AccountKeySpace, u.Change, u.ControlProgramIndex)
	if err != nil {
		return nil, nil, err
	}

	if u.Address == "" {
		sigInst.AddWitnessKeys(signer.XPubs, path, signer.Quorum)
		return txInput, sigInst, nil
	}

	address, err := common.DecodeAddress(u.Address, &consensus.ActiveNetParams)
	if err != nil {
		return nil, nil, err
	}

	sigInst.AddRawWitnessKeys(signer.XPubs, path, signer.Quorum)
	derivedXPubs := chainkd.DeriveXPubs(signer.XPubs, path)

	switch address.(type) {
	case *common.AddressWitnessPubKeyHash:
		derivedPK := derivedXPubs[0].PublicKey()
		sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness([]byte(derivedPK)))

	case *common.AddressWitnessScriptHash:
		derivedPKs := chainkd.XPubKeys(derivedXPubs)
		script, err := vmutil.P2SPMultiSigProgram(derivedPKs, signer.Quorum)
		if err != nil {
			return nil, nil, err
		}
		sigInst.WitnessComponents = append(sigInst.WitnessComponents, txbuilder.DataWitness(script))

	default:
		return nil, nil, errors.New("unsupport address type")
	}

	return txInput, sigInst, nil
}

// insertControlProgramDelayed takes a template builder and an account
// control program that hasn't been inserted to the database yet. It
// registers callbacks on the TemplateBuilder so that all of the template's
// account control programs are batch inserted if building the rest of
// the template is successful.
func (m *Manager) insertControlProgramDelayed(b *txbuilder.TemplateBuilder, acp *CtrlProgram) {
	m.delayedACPsMu.Lock()
	m.delayedACPs[b] = append(m.delayedACPs[b], acp)
	m.delayedACPsMu.Unlock()

	b.OnRollback(func() {
		m.delayedACPsMu.Lock()
		delete(m.delayedACPs, b)
		m.delayedACPsMu.Unlock()
	})
	b.OnBuild(func() error {
		m.delayedACPsMu.Lock()
		acps := m.delayedACPs[b]
		delete(m.delayedACPs, b)
		m.delayedACPsMu.Unlock()

		// Insert all of the account control programs at once.
		if len(acps) == 0 {
			return nil
		}

		return m.SaveControlPrograms(acps...)
	})
}

//DecodeVetoAction unmarshal JSON-encoded data of spend action
func (m *Manager) DecodeVetoAction(data []byte) (txbuilder.Action, error) {
	a := &vetoAction{accounts: m}
	return a, stdjson.Unmarshal(data, a)
}

type vetoAction struct {
	accounts *Manager
	bc.AssetAmount
	AccountID      string        `json:"account_id"`
	Vote           json.HexBytes `json:"vote"`
	UseUnconfirmed bool          `json:"use_unconfirmed"`
}

func (a *vetoAction) ActionType() string {
	return "veto"
}

func (a *vetoAction) Build(ctx context.Context, b *txbuilder.TemplateBuilder) error {
	var missing []string
	if a.AccountID == "" {
		missing = append(missing, "account_id")
	}
	if a.AssetId.IsZero() {
		missing = append(missing, "asset_id")
	}
	if len(missing) > 0 {
		return txbuilder.MissingFieldsError(missing...)
	}

	acct, err := a.accounts.FindByID(a.AccountID)
	if err != nil {
		return errors.Wrap(err, "get account info")
	}

	res, err := a.accounts.utxoKeeper.Reserve(a.AccountID, a.AssetId, a.Amount, a.UseUnconfirmed, a.Vote, b.MaxTime())
	if err != nil {
		return errors.Wrap(err, "reserving utxos")
	}

	// Cancel the reservation if the build gets rolled back.
	b.OnRollback(func() { a.accounts.utxoKeeper.Cancel(res.id) })
	for _, r := range res.utxos {
		txInput, sigInst, err := UtxoToInputs(acct.Signer, r)
		if err != nil {
			return errors.Wrap(err, "creating inputs")
		}

		if err = b.AddInput(txInput, sigInst); err != nil {
			return errors.Wrap(err, "adding inputs")
		}
	}

	if res.change > 0 {
		acp, err := a.accounts.CreateAddress(a.AccountID, true)
		if err != nil {
			return errors.Wrap(err, "creating control program")
		}

		// Don't insert the control program until callbacks are executed.
		a.accounts.insertControlProgramDelayed(b, acp)
		if err = b.AddOutput(types.NewVoteOutput(*a.AssetId, res.change, acp.ControlProgram, a.Vote)); err != nil {
			return errors.Wrap(err, "adding change voteOutput")
		}
	}
	return nil
}
