package consensus

import (
	"fmt"

	cstypes "github.com/tendermint/tendermint/consensus/types"
	tmproto "github.com/tendermint/tendermint/proto/tendermint/types"
	"github.com/tendermint/tendermint/types"
)

// MisbehaviorList encompasses a list of all possible behaviors
var MisbehaviorList = map[string]Misbehavior{
	"double-prevote": DoublePrevoteMisbehavior(),
}

type Misbehavior struct {
	Name string

	EnterPropose func(cs *State, height int64, round int32)

	EnterPrevote func(cs *State, height int64, round int32)

	EnterPrecommit func(cs *State, height int64, round int32)

	ReceivePrevote func(cs *State, prevote *types.Vote)

	ReceivePrecommit func(cs *State, precommit *types.Vote)

	ReceiveProposal func(cs *State, proposal *types.Proposal) error
}

// BEHAVIORS

func DefaultMisbehavior() Misbehavior {
	return Misbehavior{
		Name:             "default",
		EnterPropose:     defaultEnterPropose,
		EnterPrevote:     defaultEnterPrevote,
		EnterPrecommit:   defaultEnterPrecommit,
		ReceivePrevote:   defaultReceivePrevote,
		ReceivePrecommit: defaultReceivePrecommit,
		ReceiveProposal:  defaultReceiveProposal,
	}
}

// DoublePrevoteMisbehavior will make a node prevote both nil and a block in the same
// height and round.
func DoublePrevoteMisbehavior() Misbehavior {
	b := DefaultMisbehavior()
	b.Name = "double-prevote"
	b.EnterPrevote = func(cs *State, height int64, round int32) {

		// If a block is locked, prevote that.
		if cs.LockedBlock != nil {
			cs.Logger.Info("enterPrevote: Already locked on a block, prevoting locked block")
			cs.signAddVote(tmproto.PrevoteType, cs.LockedBlock.Hash(), cs.LockedBlockParts.Header())
			return
		}

		// If ProposalBlock is nil, prevote nil.
		if cs.ProposalBlock == nil {
			cs.Logger.Info("enterPrevote: ProposalBlock is nil")
			cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
			return
		}

		// Validate proposal block
		err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock)
		if err != nil {
			// ProposalBlock is invalid, prevote nil.
			cs.Logger.Error("enterPrevote: ProposalBlock is invalid", "err", err)
			cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
			return
		}

		if cs.sw == nil {
			cs.Logger.Error("nil switch")
			return
		}

		prevote, err := cs.signVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header())
		if err != nil {
			cs.Logger.Error("enterPrevote: Unable to sign block", "err", err)
		}

		nilPrevote, err := cs.signVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
		if err != nil {
			cs.Logger.Error("enterPrevote: Unable to sign block", "err", err)
		}

		// add our own vote
		cs.sendInternalMessage(msgInfo{&VoteMessage{prevote}, ""})

		cs.Logger.Info("Sending conflicting votes")
		peers := cs.sw.Peers().List()
		// there has to be at least two other peers connected else this behavior works normally
		for idx, peer := range peers {
			if idx%2 == 0 { // sign the proposal block
				peer.Send(VoteChannel, MustEncode(&VoteMessage{prevote}))
			} else { // sign a nil block
				peer.Send(VoteChannel, MustEncode(&VoteMessage{nilPrevote}))
			}
		}
	}
	return b
}

// DEFAULTS

func defaultEnterPropose(cs *State, height int64, round int32) {
	logger := cs.Logger.With("height", height, "round", round)
	// If we don't get the proposal and all block parts quick enough, enterPrevote
	cs.scheduleTimeout(cs.config.Propose(round), height, round, cstypes.RoundStepPropose)

	// Nothing more to do if we're not a validator
	if cs.privValidator == nil {
		logger.Debug("This node is not a validator")
		return
	}
	logger.Debug("This node is a validator")

	pubKey, err := cs.privValidator.GetPubKey()
	if err != nil {
		// If this node is a validator & proposer in the currentx round, it will
		// miss the opportunity to create a block.
		logger.Error("Error on retrival of pubkey", "err", err)
		return
	}
	address := pubKey.Address()

	// if not a validator, we're done
	if !cs.Validators.HasAddress(address) {
		logger.Debug("This node is not a validator", "addr", address, "vals", cs.Validators)
		return
	}

	if cs.isProposer(address) {
		logger.Info("enterPropose: Our turn to propose",
			"proposer",
			address,
			"privValidator",
			cs.privValidator)
		cs.decideProposal(height, round)
	} else {
		logger.Info("enterPropose: Not our turn to propose",
			"proposer",
			cs.Validators.GetProposer().Address,
			"privValidator",
			cs.privValidator)
	}
}

func defaultEnterPrevote(cs *State, height int64, round int32) {
	logger := cs.Logger.With("height", height, "round", round)

	// If a block is locked, prevote that.
	if cs.LockedBlock != nil {
		logger.Info("enterPrevote: Already locked on a block, prevoting locked block")
		cs.signAddVote(tmproto.PrevoteType, cs.LockedBlock.Hash(), cs.LockedBlockParts.Header())
		return
	}

	// If ProposalBlock is nil, prevote nil.
	if cs.ProposalBlock == nil {
		logger.Info("enterPrevote: ProposalBlock is nil")
		cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
		return
	}

	// Validate proposal block
	err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock)
	if err != nil {
		// ProposalBlock is invalid, prevote nil.
		logger.Error("enterPrevote: ProposalBlock is invalid", "err", err)
		cs.signAddVote(tmproto.PrevoteType, nil, types.PartSetHeader{})
		return
	}

	// Prevote cs.ProposalBlock
	// NOTE: the proposal signature is validated when it is received,
	// and the proposal block parts are validated as they are received (against the merkle hash in the proposal)
	logger.Info("enterPrevote: ProposalBlock is valid")
	cs.signAddVote(tmproto.PrevoteType, cs.ProposalBlock.Hash(), cs.ProposalBlockParts.Header())
}

func defaultEnterPrecommit(cs *State, height int64, round int32) {
	logger := cs.Logger.With("height", height, "round", round)

	// check for a polka
	blockID, ok := cs.Votes.Prevotes(round).TwoThirdsMajority()

	// If we don't have a polka, we must precommit nil.
	if !ok {
		if cs.LockedBlock != nil {
			logger.Info("enterPrecommit: No +2/3 prevotes during enterPrecommit while we're locked. Precommitting nil")
		} else {
			logger.Info("enterPrecommit: No +2/3 prevotes during enterPrecommit. Precommitting nil.")
		}
		cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{})
		return
	}

	// At this point +2/3 prevoted for a particular block or nil.
	_ = cs.eventBus.PublishEventPolka(cs.RoundStateEvent())

	// the latest POLRound should be this round.
	polRound, _ := cs.Votes.POLInfo()
	if polRound < round {
		panic(fmt.Sprintf("This POLRound should be %v but got %v", round, polRound))
	}

	// +2/3 prevoted nil. Unlock and precommit nil.
	if len(blockID.Hash) == 0 {
		if cs.LockedBlock == nil {
			logger.Info("enterPrecommit: +2/3 prevoted for nil.")
		} else {
			logger.Info("enterPrecommit: +2/3 prevoted for nil. Unlocking")
			cs.LockedRound = -1
			cs.LockedBlock = nil
			cs.LockedBlockParts = nil
			_ = cs.eventBus.PublishEventUnlock(cs.RoundStateEvent())
		}
		cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{})
		return
	}

	// At this point, +2/3 prevoted for a particular block.

	// If we're already locked on that block, precommit it, and update the LockedRound
	if cs.LockedBlock.HashesTo(blockID.Hash) {
		logger.Info("enterPrecommit: +2/3 prevoted locked block. Relocking")
		cs.LockedRound = round
		_ = cs.eventBus.PublishEventRelock(cs.RoundStateEvent())
		cs.signAddVote(tmproto.PrecommitType, blockID.Hash, blockID.PartSetHeader)
		return
	}

	// If +2/3 prevoted for proposal block, stage and precommit it
	if cs.ProposalBlock.HashesTo(blockID.Hash) {
		logger.Info("enterPrecommit: +2/3 prevoted proposal block. Locking", "hash", blockID.Hash)
		// Validate the block.
		if err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock); err != nil {
			panic(fmt.Sprintf("enterPrecommit: +2/3 prevoted for an invalid block: %v", err))
		}
		cs.LockedRound = round
		cs.LockedBlock = cs.ProposalBlock
		cs.LockedBlockParts = cs.ProposalBlockParts
		_ = cs.eventBus.PublishEventLock(cs.RoundStateEvent())
		cs.signAddVote(tmproto.PrecommitType, blockID.Hash, blockID.PartSetHeader)
		return
	}

	// There was a polka in this round for a block we don't have.
	// Fetch that block, unlock, and precommit nil.
	// The +2/3 prevotes for this round is the POL for our unlock.
	logger.Info("enterPrecommit: +2/3 prevotes for a block we don't have. Voting nil", "blockID", blockID)
	cs.LockedRound = -1
	cs.LockedBlock = nil
	cs.LockedBlockParts = nil
	if !cs.ProposalBlockParts.HasHeader(blockID.PartSetHeader) {
		cs.ProposalBlock = nil
		cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartSetHeader)
	}
	_ = cs.eventBus.PublishEventUnlock(cs.RoundStateEvent())
	cs.signAddVote(tmproto.PrecommitType, nil, types.PartSetHeader{})
}

func defaultReceivePrevote(cs *State, vote *types.Vote) {
	height := cs.Height
	prevotes := cs.Votes.Prevotes(vote.Round)

	// If +2/3 prevotes for a block or nil for *any* round:
	if blockID, ok := prevotes.TwoThirdsMajority(); ok {

		// There was a polka!
		// If we're locked but this is a recent polka, unlock.
		// If it matches our ProposalBlock, update the ValidBlock

		// Unlock if `cs.LockedRound < vote.Round <= cs.Round`
		// NOTE: If vote.Round > cs.Round, we'll deal with it when we get to vote.Round
		if (cs.LockedBlock != nil) &&
			(cs.LockedRound < vote.Round) &&
			(vote.Round <= cs.Round) &&
			!cs.LockedBlock.HashesTo(blockID.Hash) {

			cs.Logger.Info("Unlocking because of POL.", "lockedRound", cs.LockedRound, "POLRound", vote.Round)
			cs.LockedRound = -1
			cs.LockedBlock = nil
			cs.LockedBlockParts = nil
			_ = cs.eventBus.PublishEventUnlock(cs.RoundStateEvent())
		}

		// Update Valid* if we can.
		// NOTE: our proposal block may be nil or not what received a polka..
		if len(blockID.Hash) != 0 && (cs.ValidRound < vote.Round) && (vote.Round == cs.Round) {

			if cs.ProposalBlock.HashesTo(blockID.Hash) {
				cs.Logger.Info(
					"Updating ValidBlock because of POL.", "validRound", cs.ValidRound, "POLRound", vote.Round)
				cs.ValidRound = vote.Round
				cs.ValidBlock = cs.ProposalBlock
				cs.ValidBlockParts = cs.ProposalBlockParts
			} else {
				cs.Logger.Info(
					"Valid block we don't know about. Set ProposalBlock=nil",
					"proposal", cs.ProposalBlock.Hash(), "blockID", blockID.Hash)
				// We're getting the wrong block.
				cs.ProposalBlock = nil
			}
			if !cs.ProposalBlockParts.HasHeader(blockID.PartSetHeader) {
				cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartSetHeader)
			}
			cs.evsw.FireEvent(types.EventValidBlock, &cs.RoundState)
			_ = cs.eventBus.PublishEventValidBlock(cs.RoundStateEvent())
		}
	}

	// If +2/3 prevotes for *anything* for future round:
	switch {
	case cs.Round < vote.Round && prevotes.HasTwoThirdsAny():
		// Round-skip if there is any 2/3+ of votes ahead of us
		cs.enterNewRound(height, vote.Round)
	case cs.Round == vote.Round && cstypes.RoundStepPrevote <= cs.Step: // current round
		blockID, ok := prevotes.TwoThirdsMajority()
		if ok && (cs.isProposalComplete() || len(blockID.Hash) == 0) {
			cs.enterPrecommit(height, vote.Round)
		} else if prevotes.HasTwoThirdsAny() {
			cs.enterPrevoteWait(height, vote.Round)
		}
	case cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == vote.Round:
		// If the proposal is now complete, enter prevote of cs.Round.
		if cs.isProposalComplete() {
			cs.enterPrevote(height, cs.Round)
		}
	}

}

func defaultReceivePrecommit(cs *State, vote *types.Vote) {
	height := cs.Height
	precommits := cs.Votes.Precommits(vote.Round)
	cs.Logger.Info("Added to precommit", "vote", vote, "precommits", precommits.StringShort())

	blockID, ok := precommits.TwoThirdsMajority()
	if ok {
		// Executed as TwoThirdsMajority could be from a higher round
		cs.enterNewRound(height, vote.Round)
		cs.enterPrecommit(height, vote.Round)
		if len(blockID.Hash) != 0 {
			cs.enterCommit(height, vote.Round)
			if cs.config.SkipTimeoutCommit && precommits.HasAll() {
				cs.enterNewRound(cs.Height, 0)
			}
		} else {
			cs.enterPrecommitWait(height, vote.Round)
		}
	} else if cs.Round <= vote.Round && precommits.HasTwoThirdsAny() {
		cs.enterNewRound(height, vote.Round)
		cs.enterPrecommitWait(height, vote.Round)
	}
}

func defaultReceiveProposal(cs *State, proposal *types.Proposal) error {
	// Already have one
	// TODO: possibly catch double proposals
	if cs.Proposal != nil {
		return nil
	}

	// Does not apply
	if proposal.Height != cs.Height || proposal.Round != cs.Round {
		return nil
	}

	// Verify POLRound, which must be -1 or in range [0, proposal.Round).
	if proposal.POLRound < -1 ||
		(proposal.POLRound >= 0 && proposal.POLRound >= proposal.Round) {
		return ErrInvalidProposalPOLRound
	}

	p := proposal.ToProto()
	// Verify signature
	if !cs.Validators.GetProposer().PubKey.VerifySignature(
		types.ProposalSignBytes(cs.state.ChainID, p), proposal.Signature) {
		return ErrInvalidProposalSignature
	}

	proposal.Signature = p.Signature
	cs.Proposal = proposal
	// We don't update cs.ProposalBlockParts if it is already set.
	// This happens if we're already in cstypes.RoundStepCommit or if there is a valid block in the current round.
	// TODO: We can check if Proposal is for a different block as this is a sign of misbehavior!
	if cs.ProposalBlockParts == nil {
		cs.ProposalBlockParts = types.NewPartSetFromHeader(proposal.BlockID.PartSetHeader)
	}
	cs.Logger.Info("Received proposal", "proposal", proposal)
	return nil
}
