package accountability

import (
	"errors"
	"fmt"

	"github.com/autonity/autonity/autonity"
	"github.com/autonity/autonity/common"
	"github.com/autonity/autonity/consensus/tendermint/core/message"
	"github.com/autonity/autonity/core/types"
	"github.com/autonity/autonity/crypto"
	"github.com/autonity/autonity/eth/protocols/eth"
	"github.com/autonity/autonity/rlp"
)

var (
	errInvalidAccusation           = errors.New("invalid accusation")
	errInvalidInnocenceProof       = errors.New("invalid proof of innocence")
	errAccusationFromNoneValidator = errors.New("accusation from none validator node")
)

type InnocenceProofBuffer struct {
	accusationList []common.Hash
	proofs         map[common.Hash][]byte
}

func NewInnocenceProofBuffer() *InnocenceProofBuffer {
	buff := &InnocenceProofBuffer{
		proofs: make(map[common.Hash][]byte),
	}
	return buff
}

func (i *InnocenceProofBuffer) cacheInnocenceProof(challengeHash common.Hash, payload []byte) {
	if len(i.accusationList) >= maxNumOfInnocenceProofCached {
		// remove the LRU one.
		delete(i.proofs, i.accusationList[0])
		i.accusationList = i.accusationList[1:]
	}
	i.accusationList = append(i.accusationList, challengeHash)
	i.proofs[challengeHash] = payload
}

func (i *InnocenceProofBuffer) getInnocenceProofFromCache(challengeHash common.Hash) []byte {
	proof, ok := i.proofs[challengeHash]
	if !ok {
		return nil
	}
	return proof
}

// this function take accountability events: an accusation or an innocence proof event to handle off chain accusation
// protocol. It returns error to freeze remote peer for 30s by according to dev p2p protocol to prevent from DoS attack.
// NOTE: sender is the p2p sender of the offchain accountability message
func (fd *FaultDetector) handleOffChainAccountabilityEvent(payload []byte, sender common.Address) error {
	// drop peer if the accusation exceed the rate limit during the last 1 seconds.
	err := fd.accusationRateLimiter.timeLimiter.Allow(sender)
	if err != nil {
		fd.logger.Error("accountability abuse detected!", "err", err)
		return err
	}

	// drop peer if it sent duplicated accusation event.
	msgHash := crypto.Hash(payload)
	err = fd.accusationRateLimiter.duplicateLimiter.Allow(sender, msgHash)
	if err != nil {
		fd.logger.Error("duplicated accusation from peer", "err", err)
		return err
	}

	// send response if we have processed it recently.
	cachedProof := fd.innocenceProofBuff.getInnocenceProofFromCache(msgHash)
	if cachedProof != nil {
		fd.sendOffChainInnocenceProof(sender, cachedProof)
		return nil
	}

	// handle a brand-new accusation event from rlp decoding of proof.
	proof, err := decodeRawProof(payload)
	if err != nil {
		return err
	}

	// drop peer if the proof does not come from a committee member
	msgHeight := proof.Message.H()
	committee, err := fd.blockchain.CommitteeByHeight(msgHeight)
	if err != nil {
		fd.logger.Error("cannot get committee for height", "error", err, "height", msgHeight)
		return err
	}

	memberShip := committee.MemberByAddress(sender)
	if memberShip == nil {
		fd.logger.Info("accusation from a non validator node", "height", msgHeight, "sender", sender)
		return errAccusationFromNoneValidator
	}

	// drop peer if one send more than the number of accusations could be produced by rule engine over a height.
	err = fd.accusationRateLimiter.heightLimiter.Allow(sender, msgHeight)
	if err != nil {
		fd.logger.Info("over rated accusation over a height", "error", err)
		return err
	}

	// verify proof signatures at last since it is more cost-full than other checkers.
	if err = verifyProofSignatures(committee, proof); err != nil {
		return err
	}

	// handle accusation and provide innocence proof.
	if proof.Type == autonity.Accusation {
		return fd.handleOffChainAccusation(proof, sender, msgHash, committee)
	}

	// handle innocence proof and to withdraw those pending accusation.
	if proof.Type == autonity.Innocence {
		return fd.handleOffChainProofOfInnocence(proof, sender, committee)
	}
	return fmt.Errorf("wrong proof type for off chain accusation events")
}

func (fd *FaultDetector) handleOffChainAccusation(accusation *Proof, sender common.Address, accusationHash common.Hash,
	committee *types.Committee) error {
	// offenderIndex range was checked, now if the suspected msg's sender is not current peer,
	// then it would be a DoS attack, drop the peer with an error returned.
	if committee.Members[accusation.OffenderIndex].Address != fd.address {
		return errInvalidAccusation
	}

	currentCoreHeight := fd.blockchain.CurrentBlock().NumberU64() + 1
	accountabilityParams, err := fd.blockchain.AccountabilityParamsByHeight(currentCoreHeight)
	if err != nil {
		// this shouldn't happen unless the node has some aggressive state pruning in place
		fd.logger.Error("cannot fetch accountability params", "block", currentCoreHeight, "err", err)
		return nil // do not drop
	}

	if err := preVerifyAccusation(accusation.Message, currentCoreHeight, accountabilityParams.Range.Uint64(), accountabilityParams.Delta.Uint64(), accountabilityParams.GracePeriod.Uint64()); err != nil {
		// such error could be due to the timing and delay, thus we don't drop the remote peer connection.
		return nil
	}

	// if the suspicious message is for a value that got committed in the same height --> reject accusation. This could
	// happen due to the timing and delay.
	if fd.blockchain.GetBlock(accusation.Message.Value(), accusation.Message.H()) != nil {
		fd.logger.Info("reject an accusation over a committed block", "sender", sender, "block", accusation.Message.H())
		return nil
	}

	// check if the accusation sent by remote peer is valid or not, an invalid accusation will drop sender's peer.
	if !verifyAccusation(accusation, committee) {
		return errInvalidAccusation
	}

	// query innocence proof for accusation from msg store.
	ev, err := fd.innocenceProof(accusation, committee)
	if err != nil {
		fd.logger.Warn("cannot collect ev of innocence for the accusation", "err", err)
		return nil
	}

	if len(ev.RawProof) > eth.MaxMessageSize {
		fd.logger.Error("the innocence ev is over size than 10MB")
		return nil
	}

	// buffer the innocence proof for such accusation in case of same accusation from other peers.
	fd.innocenceProofBuff.cacheInnocenceProof(accusationHash, ev.RawProof)
	// send the innocence proof to challenger.
	fd.sendOffChainInnocenceProof(sender, ev.RawProof)
	return nil
}

func (fd *FaultDetector) handleOffChainProofOfInnocence(proof *Proof, sender common.Address, committee *types.Committee) error {
	// if the sender is not the one being challenged against, then drop the peer by returning error.
	if committee.Members[proof.OffenderIndex].Address != sender {
		return errInvalidInnocenceProof
	}

	// check if the proof is valid, an invalid proof of innocence will freeze the peer connection.
	if !verifyInnocenceProof(proof, committee) {
		return errInvalidInnocenceProof
	}

	// the proof is valid, withdraw the off chain challenge.
	fd.removeOffChainAccusation(proof)
	return nil
}

func (fd *FaultDetector) addOffChainAccusation(accusation *Proof) {
	fd.offChainAccusationsMu.Lock()
	defer fd.offChainAccusationsMu.Unlock()
	fd.offChainAccusations = append(fd.offChainAccusations, accusation)
}

// remove off chain accusation is called when there is valid innocence proof been received or on when the timer is expired,
// the accusation to be escalated as on chain accusation.
func (fd *FaultDetector) removeOffChainAccusation(innocenceProof *Proof) {
	fd.offChainAccusationsMu.Lock()
	defer fd.offChainAccusationsMu.Unlock()
	i := 0
	find := false
	for ; i < len(fd.offChainAccusations); i++ {
		if fd.offChainAccusations[i].Rule == innocenceProof.Rule && fd.offChainAccusations[i].Type == autonity.Accusation &&
			fd.offChainAccusations[i].Message.Hash() == innocenceProof.Message.Hash() {
			// release the event's memory
			fd.offChainAccusations[i] = nil
			find = true
			break
		}
	}

	// release the pointer from slice.
	if find {
		fd.offChainAccusations = append(fd.offChainAccusations[:i], fd.offChainAccusations[i+1:]...)
	}
}

func (fd *FaultDetector) getExpiredOffChainAccusation(currentCoreHeight uint64) []*Proof {
	fd.offChainAccusationsMu.RLock()
	defer fd.offChainAccusationsMu.RUnlock()
	var expiredOnes []*Proof

	currentAccountabilityParams, err := fd.blockchain.AccountabilityParamsByHeight(currentCoreHeight)
	if err != nil {
		fd.logger.Error("Cannot fetch accountability params", "block", currentCoreHeight, "err", err)
		return expiredOnes
	}

	for _, proof := range fd.offChainAccusations {
		// NOTE: accusations for message at height h is generated at height h + delta by the fault detector
		// then we have up to h + delta + offchainWindow to resolve it offchain
		if currentCoreHeight-proof.Message.H() > (currentAccountabilityParams.Delta.Uint64() + offChainAccusationProofWindow) {
			expiredOnes = append(expiredOnes, proof)
		}
	}
	return expiredOnes
}

// if those off chain challenge have no innocence proof within the proof window, then escalate them on-chain.
func (fd *FaultDetector) escalateExpiredAccusations(currentCoreHeight uint64) {
	escalatedOnes := fd.getExpiredOffChainAccusation(currentCoreHeight)
	for _, accusation := range escalatedOnes {
		committee, err := fd.blockchain.CommitteeByHeight(accusation.Message.H())
		if err != nil {
			panic(fmt.Sprintf("cannot get committee of height: %d", accusation.Message.H()))
		}
		offender := committee.Members[accusation.OffenderIndex].Address
		fd.removeOffChainAccusation(accusation)
		p := fd.eventFromProof(accusation, offender)
		// push it to the on chain accountability event list
		fd.pendingEvents = append(fd.pendingEvents, p)
	}
}

// send the off chain accusation msg to the peer suspected
func (fd *FaultDetector) sendOffChainAccusationMsg(accusation *Proof, committee *types.Committee) {
	// send the off chain accusation msg to the suspected one,
	if fd.broadcaster == nil {
		fd.logger.Warn("p2p protocol handler is not ready yet")
		return
	}

	target := committee.Members[accusation.OffenderIndex].Address
	peer, ok := fd.broadcaster.FindPeer(target)
	if !ok {
		//todo: if we need to gossip this message in case of there are no direct peer connection.
		fd.logger.Debug("No direct p2p connection with suspect")
		return
	}

	rProof, err := rlp.EncodeToBytes(accusation)
	if err != nil {
		fd.logger.Warn("cannot rlp encode accusation", "err", err)
		return
	}

	fd.logger.Info("Attempting direct p2p resolution..", "suspect", target)
	go peer.Send(message.AccountabilityNetworkMsg, rProof) //nolint
}

// sendOffChainInnocenceProof, send an innocence proof to receiver peer.
func (fd *FaultDetector) sendOffChainInnocenceProof(receiver common.Address, payload []byte) {
	if fd.broadcaster == nil {
		fd.logger.Warn("p2p protocol handler is not ready yet")
		return
	}
	peer, ok := fd.broadcaster.FindPeer(receiver)
	if !ok {
		//todo: if we need to gossip this message in case of there are no direct peer connection.
		fd.logger.Debug("no peer connection for off chain innocence proof event")
		return
	}

	fd.logger.Info("Sending requested innocence proof", "addr", receiver)
	go peer.Send(message.AccountabilityNetworkMsg, payload) //nolint
}
