package state

import (
	"encoding/hex"
	"sort"

	"github.com/vapor/common/arithmetic"
	"github.com/vapor/config"
	"github.com/vapor/consensus"
	"github.com/vapor/crypto/ed25519/chainkd"
	"github.com/vapor/errors"
	"github.com/vapor/math/checked"
	"github.com/vapor/protocol/bc"
	"github.com/vapor/protocol/bc/types"
)

// fedConsensusPath is used to derive federation root xpubs for signing blocks
var fedConsensusPath = [][]byte{
	[]byte{0xff, 0xff, 0xff, 0xff},
	[]byte{0xff, 0x00, 0x00, 0x00},
	[]byte{0xff, 0xff, 0xff, 0xff},
	[]byte{0xff, 0x00, 0x00, 0x00},
	[]byte{0xff, 0x00, 0x00, 0x00},
}

// ConsensusNode represents a consensus node
type ConsensusNode struct {
	XPub    chainkd.XPub
	VoteNum uint64
	Order   uint64
}

type byVote []*ConsensusNode

func (c byVote) Len() int { return len(c) }
func (c byVote) Less(i, j int) bool {
	return c[i].VoteNum > c[j].VoteNum || (c[i].VoteNum == c[j].VoteNum && c[i].XPub.String() > c[j].XPub.String())
}
func (c byVote) Swap(i, j int) { c[i], c[j] = c[j], c[i] }

// CoinbaseReward contains receiver and reward
type CoinbaseReward struct {
	Amount         uint64
	ControlProgram []byte
}

// SortByAmount implements sort.Interface for CoinbaseReward slices
type SortByAmount []CoinbaseReward

func (a SortByAmount) Len() int      { return len(a) }
func (a SortByAmount) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a SortByAmount) Less(i, j int) bool {
	return a[i].Amount > a[j].Amount || (a[i].Amount == a[j].Amount && hex.EncodeToString(a[i].ControlProgram) > hex.EncodeToString(a[j].ControlProgram))
}

// CalCoinbaseReward calculate the coinbase reward for block
func CalCoinbaseReward(block *types.Block) (*CoinbaseReward, error) {
	result := &CoinbaseReward{}
	if len(block.Transactions) > 0 && len(block.Transactions[0].Outputs) > 0 {
		result.ControlProgram = block.Transactions[0].Outputs[0].ControlProgram()
	} else {
		return nil, errors.New("not found coinbase receiver")
	}

	result.Amount = consensus.BlockSubsidy(block.BlockHeader.Height)
	for _, tx := range block.Transactions {
		txFee, err := arithmetic.CalculateTxFee(tx)
		if err != nil {
			return nil, errors.Wrap(checked.ErrOverflow, "calculate transaction fee")
		}

		result.Amount += txFee
	}
	return result, nil
}

// CalcVoteSeq calculate the vote sequence
// seq 0 is the genesis block
// seq 1 is the the block height 1, to block height RoundVoteBlockNums
// seq 2 is the block height RoundVoteBlockNums + 1 to block height 2 * RoundVoteBlockNums
// consensus node of the current round is the final result of previous round
func CalcVoteSeq(blockHeight uint64) uint64 {
	if blockHeight == 0 {
		return 0
	}
	return (blockHeight-1)/consensus.ActiveNetParams.RoundVoteBlockNums + 1
}

// ConsensusResult represents a snapshot of each round of DPOS voting
// Seq indicates the sequence of current votes, which start from zero
// NumOfVote indicates the number of votes each consensus node receives, the key of map represent public key
// CoinbaseReward indicates the coinbase receiver and reward
type ConsensusResult struct {
	Seq            uint64
	NumOfVote      map[string]uint64
	CoinbaseReward map[string]uint64
	BlockHash      bc.Hash
	BlockHeight    uint64
}

// ApplyBlock calculate the consensus result for new block
func (c *ConsensusResult) ApplyBlock(block *types.Block) error {
	if c.BlockHash != block.PreviousBlockHash {
		return errors.New("block parent hash is not equals last block hash of vote result")
	}

	if err := c.AttachCoinbaseReward(block); err != nil {
		return err
	}

	for _, tx := range block.Transactions {
		if err := c.ApplyTransaction(tx); err != nil {
			return err
		}
	}

	c.BlockHash = block.Hash()
	c.BlockHeight = block.Height
	c.Seq = CalcVoteSeq(block.Height)
	return nil
}

// ApplyTransaction calculate the consensus result for transaction
func (c *ConsensusResult) ApplyTransaction(tx *types.Tx) error {
	for _, input := range tx.Inputs {
		vetoInput, ok := input.TypedInput.(*types.VetoInput)
		if !ok {
			continue
		}

		pubkey := hex.EncodeToString(vetoInput.Vote)
		c.NumOfVote[pubkey], ok = checked.SubUint64(c.NumOfVote[pubkey], vetoInput.Amount)
		if !ok {
			return checked.ErrOverflow
		}

		if c.NumOfVote[pubkey] == 0 {
			delete(c.NumOfVote, pubkey)
		}
	}

	for _, output := range tx.Outputs {
		voteOutput, ok := output.TypedOutput.(*types.VoteOutput)
		if !ok {
			continue
		}

		pubkey := hex.EncodeToString(voteOutput.Vote)
		if c.NumOfVote[pubkey], ok = checked.AddUint64(c.NumOfVote[pubkey], voteOutput.Amount); !ok {
			return checked.ErrOverflow
		}
	}
	return nil
}

// AttachCoinbaseReward attach coinbase reward
func (c *ConsensusResult) AttachCoinbaseReward(block *types.Block) error {
	reward, err := CalCoinbaseReward(block)
	if err != nil {
		return err
	}

	if block.Height%consensus.ActiveNetParams.RoundVoteBlockNums == 1 {
		c.CoinbaseReward = map[string]uint64{}
	}

	var ok bool
	program := hex.EncodeToString(reward.ControlProgram)
	c.CoinbaseReward[program], ok = checked.AddUint64(c.CoinbaseReward[program], reward.Amount)
	if !ok {
		return checked.ErrOverflow
	}
	return nil
}

// ConsensusNodes returns all consensus nodes
func (c *ConsensusResult) ConsensusNodes() (map[string]*ConsensusNode, error) {
	var nodes []*ConsensusNode
	for pubkey, voteNum := range c.NumOfVote {
		if voteNum >= consensus.ActiveNetParams.MinConsensusNodeVoteNum {
			var xpub chainkd.XPub
			if err := xpub.UnmarshalText([]byte(pubkey)); err != nil {
				return nil, err
			}

			nodes = append(nodes, &ConsensusNode{XPub: xpub, VoteNum: voteNum})
		}
	}
	// In principle, there is no need to sort all voting nodes.
	// if there is a performance problem, consider the optimization later.
	sort.Sort(byVote(nodes))
	result := make(map[string]*ConsensusNode)
	for i := 0; i < len(nodes) && int64(i) < consensus.ActiveNetParams.NumOfConsensusNode; i++ {
		nodes[i].Order = uint64(i)
		result[nodes[i].XPub.String()] = nodes[i]
	}

	if len(result) != 0 {
		return result, nil
	}
	return federationNodes(), nil
}

// DetachBlock calculate the consensus result for detach block
func (c *ConsensusResult) DetachBlock(block *types.Block) error {
	if c.BlockHash != block.Hash() {
		return errors.New("block hash is not equals last block hash of vote result")
	}

	if err := c.DetachCoinbaseReward(block); err != nil {
		return err
	}

	for i := len(block.Transactions) - 1; i >= 0; i-- {
		tx := block.Transactions[i]
		for _, input := range tx.Inputs {
			vetoInput, ok := input.TypedInput.(*types.VetoInput)
			if !ok {
				continue
			}

			pubkey := hex.EncodeToString(vetoInput.Vote)
			if c.NumOfVote[pubkey], ok = checked.AddUint64(c.NumOfVote[pubkey], vetoInput.Amount); !ok {
				return checked.ErrOverflow
			}
		}

		for _, output := range tx.Outputs {
			voteOutput, ok := output.TypedOutput.(*types.VoteOutput)
			if !ok {
				continue
			}

			pubkey := hex.EncodeToString(voteOutput.Vote)
			c.NumOfVote[pubkey], ok = checked.SubUint64(c.NumOfVote[pubkey], voteOutput.Amount)
			if !ok {
				return checked.ErrOverflow
			}

			if c.NumOfVote[pubkey] == 0 {
				delete(c.NumOfVote, pubkey)
			}
		}
	}

	c.BlockHash = block.PreviousBlockHash
	c.BlockHeight = block.Height - 1
	c.Seq = CalcVoteSeq(block.Height - 1)
	return nil
}

// DetachCoinbaseReward detach coinbase reward
func (c *ConsensusResult) DetachCoinbaseReward(block *types.Block) error {
	reward, err := CalCoinbaseReward(block)
	if err != nil {
		return err
	}

	var ok bool
	program := hex.EncodeToString(reward.ControlProgram)
	if c.CoinbaseReward[program], ok = checked.SubUint64(c.CoinbaseReward[program], reward.Amount); !ok {
		return checked.ErrOverflow
	}

	if c.CoinbaseReward[program] == 0 {
		delete(c.CoinbaseReward, program)
	}

	if block.Height%consensus.ActiveNetParams.RoundVoteBlockNums == 1 {
		c.CoinbaseReward = map[string]uint64{}
		for i, output := range block.Transactions[0].Outputs {
			if i == 0 {
				continue
			}
			program := output.ControlProgram()
			c.CoinbaseReward[hex.EncodeToString(program)] = output.AssetAmount().Amount
		}
	}
	return nil
}

// Fork copy the ConsensusResult struct
func (c *ConsensusResult) Fork() *ConsensusResult {
	f := &ConsensusResult{
		Seq:            c.Seq,
		NumOfVote:      map[string]uint64{},
		CoinbaseReward: map[string]uint64{},
		BlockHash:      c.BlockHash,
		BlockHeight:    c.BlockHeight,
	}

	for key, value := range c.NumOfVote {
		f.NumOfVote[key] = value
	}

	for key, value := range c.CoinbaseReward {
		f.CoinbaseReward[key] = value
	}
	return f
}

// IsFinalize check if the result is end of consensus round
func (c *ConsensusResult) IsFinalize() bool {
	return c.BlockHeight%consensus.ActiveNetParams.RoundVoteBlockNums == 0
}

// GetCoinbaseRewards convert into CoinbaseReward array and sort it by amount
func (c *ConsensusResult) GetCoinbaseRewards(blockHeight uint64) ([]CoinbaseReward, error) {
	rewards := []CoinbaseReward{}
	if blockHeight%consensus.ActiveNetParams.RoundVoteBlockNums != 0 {
		return rewards, nil
	}

	for p, amount := range c.CoinbaseReward {
		program, err := hex.DecodeString(p)
		if err != nil {
			return nil, err
		}

		rewards = append(rewards, CoinbaseReward{
			Amount:         amount,
			ControlProgram: program,
		})
	}
	sort.Sort(SortByAmount(rewards))
	return rewards, nil
}

func federationNodes() map[string]*ConsensusNode {
	consensusResult := map[string]*ConsensusNode{}
	for i, xpub := range config.CommonConfig.Federation.Xpubs {
		derivedXPub := xpub.Derive(fedConsensusPath)
		consensusResult[derivedXPub.String()] = &ConsensusNode{XPub: derivedXPub, VoteNum: 0, Order: uint64(i)}
	}
	return consensusResult
}
