package actors

import (
	"github.com/ipfs/go-cid"

	"github.com/filecoin-project/lotus/chain/actors/aerrors"
	"github.com/filecoin-project/lotus/chain/address"
	"github.com/filecoin-project/lotus/chain/types"

	cbg "github.com/whyrusleeping/cbor-gen"
)

type MultiSigActor struct{}
type MultiSigActorState struct {
	Signers  []address.Address
	Required uint64
	NextTxID uint64

	InitialBalance types.BigInt
	StartingBlock  uint64
	UnlockDuration uint64

	//TODO: make this map/sharray/whatever
	Transactions []MTransaction
}

func (msas MultiSigActorState) canSpend(act *types.Actor, amnt types.BigInt, height uint64) bool {
	if msas.UnlockDuration == 0 {
		return true
	}

	offset := height - msas.StartingBlock
	if offset > msas.UnlockDuration {
		return true
	}

	minBalance := types.BigDiv(msas.InitialBalance, types.NewInt(msas.UnlockDuration))
	minBalance = types.BigMul(minBalance, types.NewInt(offset))
	return !minBalance.LessThan(types.BigSub(act.Balance, amnt))
}

func (msas MultiSigActorState) isSigner(addr address.Address) bool {
	for _, s := range msas.Signers {
		if s == addr {
			return true
		}
	}
	return false
}

func (msas MultiSigActorState) getTransaction(txid uint64) (*MTransaction, ActorError) {
	if txid >= uint64(len(msas.Transactions)) {
		return nil, aerrors.Newf(1, "could not get transaction (numbers of tx %d,want to get txid %d)", len(msas.Transactions), txid)
	}
	return &msas.Transactions[txid], nil
}

type MTransaction struct {
	Created uint64 // NOT USED ??
	TxID    uint64

	To     address.Address
	Value  types.BigInt
	Method uint64
	Params []byte

	Approved []address.Address
	Complete bool
	Canceled bool
	RetCode  uint64
}

func (tx MTransaction) Active() ActorError {
	if tx.Complete {
		return aerrors.New(2, "transaction already completed")
	}
	if tx.Canceled {
		return aerrors.New(3, "transaction canceled")
	}
	return nil
}

type musigMethods struct {
	MultiSigConstructor uint64
	Propose             uint64
	Approve             uint64
	Cancel              uint64
	ClearCompleted      uint64
	AddSigner           uint64
	RemoveSigner        uint64
	SwapSigner          uint64
	ChangeRequirement   uint64
}

var MultiSigMethods = musigMethods{1, 2, 3, 4, 5, 6, 7, 8, 9}

func (msa MultiSigActor) Exports() []interface{} {
	return []interface{}{
		1: msa.MultiSigConstructor,
		2: msa.Propose,
		3: msa.Approve,
		4: msa.Cancel,
		//5: msa.ClearCompleted,
		6: msa.AddSigner,
		7: msa.RemoveSigner,
		8: msa.SwapSigner,
		9: msa.ChangeRequirement,
	}
}

type MultiSigConstructorParams struct {
	Signers        []address.Address
	Required       uint64
	UnlockDuration uint64
}

func (MultiSigActor) MultiSigConstructor(act *types.Actor, vmctx types.VMContext,
	params *MultiSigConstructorParams) ([]byte, ActorError) {
	self := &MultiSigActorState{
		Signers:  params.Signers,
		Required: params.Required,
	}

	if params.UnlockDuration != 0 {
		self.InitialBalance = vmctx.Message().Value
		self.UnlockDuration = params.UnlockDuration
		self.StartingBlock = vmctx.BlockHeight()
	}

	head, err := vmctx.Storage().Put(self)
	if err != nil {
		return nil, aerrors.Wrap(err, "could not put new head")
	}
	err = vmctx.Storage().Commit(EmptyCBOR, head)
	if err != nil {
		return nil, aerrors.Wrap(err, "could not commit new head")
	}
	return nil, nil
}

type MultiSigProposeParams struct {
	To     address.Address
	Value  types.BigInt
	Method uint64
	Params []byte
}

func (MultiSigActor) load(vmctx types.VMContext) (cid.Cid, *MultiSigActorState, ActorError) {
	var self MultiSigActorState
	head := vmctx.Storage().GetHead()

	err := vmctx.Storage().Get(head, &self)
	if err != nil {
		return cid.Undef, nil, aerrors.Wrap(err, "could not get self")
	}
	return head, &self, nil
}

func (msa MultiSigActor) loadAndVerify(vmctx types.VMContext) (cid.Cid, *MultiSigActorState, ActorError) {
	head, self, err := msa.load(vmctx)
	if err != nil {
		return cid.Undef, nil, err
	}

	if !self.isSigner(vmctx.Message().From) {
		return cid.Undef, nil, aerrors.New(1, "not authorized")
	}
	return head, self, nil
}

func (MultiSigActor) save(vmctx types.VMContext, oldHead cid.Cid, self *MultiSigActorState) ActorError {
	newHead, err := vmctx.Storage().Put(self)
	if err != nil {
		return aerrors.Wrap(err, "could not put new head")
	}
	err = vmctx.Storage().Commit(oldHead, newHead)
	if err != nil {
		return aerrors.Wrap(err, "could not commit new head")
	}
	return nil

}

func (msa MultiSigActor) Propose(act *types.Actor, vmctx types.VMContext,
	params *MultiSigProposeParams) ([]byte, ActorError) {

	head, self, err := msa.loadAndVerify(vmctx)
	if err != nil {
		return nil, err
	}

	txid := self.NextTxID
	self.NextTxID++

	{
		tx := MTransaction{
			TxID:     txid,
			To:       params.To,
			Value:    params.Value,
			Method:   params.Method,
			Params:   params.Params,
			Approved: []address.Address{vmctx.Message().From},
		}
		self.Transactions = append(self.Transactions, tx)
	}

	tx, err := self.getTransaction(txid)
	if err != nil {
		return nil, err
	}

	if self.Required == 1 {
		if !self.canSpend(act, tx.Value, vmctx.BlockHeight()) {
			return nil, aerrors.New(100, "transaction amount exceeds available")
		}
		_, err := vmctx.Send(tx.To, tx.Method, tx.Value, tx.Params)
		if aerrors.IsFatal(err) {
			return nil, err
		}
		tx.RetCode = uint64(aerrors.RetCode(err))
		tx.Complete = true
	}

	err = msa.save(vmctx, head, self)
	if err != nil {
		return nil, aerrors.Wrap(err, "saving state")
	}

	// REVIEW: On one hand, I like being very explicit about how we're doing the serialization
	// on the other, maybe we shouldnt do direct calls to underlying serialization libs?
	return cbg.CborEncodeMajorType(cbg.MajUnsignedInt, tx.TxID), nil
}

type MultiSigTxID struct {
	TxID uint64
}

func (msa MultiSigActor) Approve(act *types.Actor, vmctx types.VMContext,
	params *MultiSigTxID) ([]byte, ActorError) {

	head, self, err := msa.loadAndVerify(vmctx)
	if err != nil {
		return nil, err
	}

	tx, err := self.getTransaction(params.TxID)
	if err != nil {
		return nil, err
	}

	if err := tx.Active(); err != nil {
		return nil, aerrors.Wrap(err, "could not approve")
	}

	for _, signer := range tx.Approved {
		if signer == vmctx.Message().From {
			return nil, aerrors.New(4, "already signed this message")
		}
	}
	tx.Approved = append(tx.Approved, vmctx.Message().From)
	if uint64(len(tx.Approved)) >= self.Required {
		if !self.canSpend(act, tx.Value, vmctx.BlockHeight()) {
			return nil, aerrors.New(100, "transaction amount exceeds available")
		}
		_, err := vmctx.Send(tx.To, tx.Method, tx.Value, tx.Params)
		if aerrors.IsFatal(err) {
			return nil, err
		}
		tx.RetCode = uint64(aerrors.RetCode(err))
		tx.Complete = true
	}

	return nil, msa.save(vmctx, head, self)
}

func (msa MultiSigActor) Cancel(act *types.Actor, vmctx types.VMContext,
	params *MultiSigTxID) ([]byte, ActorError) {

	head, self, err := msa.loadAndVerify(vmctx)
	if err != nil {
		return nil, err
	}

	tx, err := self.getTransaction(params.TxID)
	if err != nil {
		return nil, err
	}

	if err := tx.Active(); err != nil {
		return nil, aerrors.Wrap(err, "could not cancel")
	}

	proposer := tx.Approved[0]
	if proposer != vmctx.Message().From && self.isSigner(proposer) {
		return nil, aerrors.New(4, "cannot cancel another signers transaction")
	}
	tx.Canceled = true

	return nil, msa.save(vmctx, head, self)
}

type MultiSigAddSignerParam struct {
	Signer   address.Address
	Increase bool
}

func (msa MultiSigActor) AddSigner(act *types.Actor, vmctx types.VMContext,
	params *MultiSigAddSignerParam) ([]byte, ActorError) {

	head, self, err := msa.load(vmctx)
	if err != nil {
		return nil, err
	}

	msg := vmctx.Message()
	if msg.From != msg.To {
		return nil, aerrors.New(4, "add signer must be called by wallet itself")
	}
	if self.isSigner(params.Signer) {
		return nil, aerrors.New(5, "new address is already a signer")
	}

	self.Signers = append(self.Signers, params.Signer)
	if params.Increase {
		self.Required = self.Required + 1
	}

	return nil, msa.save(vmctx, head, self)
}

type MultiSigRemoveSignerParam struct {
	Signer   address.Address
	Decrease bool
}

func (msa MultiSigActor) RemoveSigner(act *types.Actor, vmctx types.VMContext,
	params *MultiSigRemoveSignerParam) ([]byte, ActorError) {

	head, self, err := msa.load(vmctx)
	if err != nil {
		return nil, err
	}

	msg := vmctx.Message()
	if msg.From != msg.To {
		return nil, aerrors.New(4, "remove signer must be called by wallet itself")
	}
	if !self.isSigner(params.Signer) {
		return nil, aerrors.New(5, "given address was not a signer")
	}

	newSigners := make([]address.Address, 0, len(self.Signers)-1)
	for _, s := range self.Signers {
		if s != params.Signer {
			newSigners = append(newSigners, s)
		}
	}
	if params.Decrease || uint64(len(self.Signers)-1) < self.Required {
		self.Required = self.Required - 1
	}

	self.Signers = newSigners

	return nil, msa.save(vmctx, head, self)
}

type MultiSigSwapSignerParams struct {
	From address.Address
	To   address.Address
}

func (msa MultiSigActor) SwapSigner(act *types.Actor, vmctx types.VMContext,
	params *MultiSigSwapSignerParams) ([]byte, ActorError) {

	head, self, err := msa.load(vmctx)
	if err != nil {
		return nil, err
	}

	msg := vmctx.Message()
	if msg.From != msg.To {
		return nil, aerrors.New(4, "swap signer must be called by wallet itself")
	}

	if !self.isSigner(params.From) {
		return nil, aerrors.New(5, "given old address was not a signer")
	}
	if self.isSigner(params.To) {
		return nil, aerrors.New(6, "given new address was already a signer")
	}

	newSigners := make([]address.Address, 0, len(self.Signers))
	for _, s := range self.Signers {
		if s != params.From {
			newSigners = append(newSigners, s)
		}
	}
	newSigners = append(newSigners, params.To)
	self.Signers = newSigners

	return nil, msa.save(vmctx, head, self)
}

type MultiSigChangeReqParams struct {
	Req uint64
}

func (msa MultiSigActor) ChangeRequirement(act *types.Actor, vmctx types.VMContext,
	params *MultiSigChangeReqParams) ([]byte, ActorError) {

	head, self, err := msa.load(vmctx)
	if err != nil {
		return nil, err
	}

	msg := vmctx.Message()
	if msg.From != msg.To {
		return nil, aerrors.New(4, "change requirement must be called by wallet itself")
	}

	if params.Req < 1 {
		return nil, aerrors.New(5, "requirement must be at least 1")
	}

	if params.Req > uint64(len(self.Signers)) {
		return nil, aerrors.New(6, "requirement must be at most the numbers of signers")
	}

	self.Required = params.Req
	return nil, msa.save(vmctx, head, self)
}