// Copyright 2020 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

package eth

import (
	"errors"
	"fmt"
	"math/big"

	"github.com/ethereum/go-ethereum/core"
	"github.com/ethereum/go-ethereum/core/types"
	"github.com/ethereum/go-ethereum/eth/protocols/eth"
	"github.com/ethereum/go-ethereum/p2p/enode"
	"github.com/ethereum/go-ethereum/rlp"
)

// handleInboundVote decodes a VotePacket payload and forwards it to the
// V2 engine via the bft.Bfter dispatcher. Returns nil if BFT is not yet
// wired (e.g. pre-V2 chain config) so the peer connection isn't dropped.
func (h *handler) handleInboundVote(peer *eth.Peer, packet *eth.VotePacket) error {
	b := h.getOrInitBft()
	if b == nil {
		return nil
	}
	var vote types.Vote
	if err := rlp.DecodeBytes(packet.Vote, &vote); err != nil {
		peer.Log().Debug("XDPoS2: bad VotePacket payload", "err", err)
		return nil
	}
	return b.Vote(peer.ID(), &vote)
}

// handleInboundTimeout — see handleInboundVote.
func (h *handler) handleInboundTimeout(peer *eth.Peer, packet *eth.TimeoutPacket) error {
	b := h.getOrInitBft()
	if b == nil {
		return nil
	}
	var timeout types.Timeout
	if err := rlp.DecodeBytes(packet.Timeout, &timeout); err != nil {
		peer.Log().Debug("XDPoS2: bad TimeoutPacket payload", "err", err)
		return nil
	}
	return b.Timeout(peer.ID(), &timeout)
}

// handleInboundSyncInfo — see handleInboundVote.
func (h *handler) handleInboundSyncInfo(peer *eth.Peer, packet *eth.SyncInfoPacket) error {
	b := h.getOrInitBft()
	if b == nil {
		return nil
	}
	var syncInfo types.SyncInfo
	if err := rlp.DecodeBytes(packet.SyncInfo, &syncInfo); err != nil {
		peer.Log().Debug("XDPoS2: bad SyncInfoPacket payload", "err", err)
		return nil
	}
	return b.SyncInfo(peer.ID(), &syncInfo)
}

// ethHandler implements the eth.Backend interface to handle the various network
// packets that are sent as replies or broadcasts.
type ethHandler handler

func (h *ethHandler) Chain() *core.BlockChain { return h.chain }
func (h *ethHandler) TxPool() eth.TxPool      { return h.txpool }

// RunPeer is invoked when a peer joins on the `eth` protocol.
func (h *ethHandler) RunPeer(peer *eth.Peer, hand eth.Handler) error {
	return (*handler)(h).runEthPeer(peer, hand)
}

// PeerInfo retrieves all known `eth` information about a peer.
func (h *ethHandler) PeerInfo(id enode.ID) interface{} {
	if p := h.peers.peer(id.String()); p != nil {
		return p.info()
	}
	return nil
}

// AcceptTxs retrieves whether transaction processing is enabled on the node
// or if inbound transactions should simply be dropped.
func (h *ethHandler) AcceptTxs() bool {
	return h.synced.Load()
}

// Handle is invoked from a peer's message handler when it receives a new remote
// message that the handler couldn't consume and serve itself.
func (h *ethHandler) Handle(peer *eth.Peer, packet eth.Packet) error {
	// Consume any broadcasts and announces, forwarding the rest to the downloader
	switch packet := packet.(type) {
	case *eth.NewPooledTransactionHashesPacket:
		return h.txFetcher.Notify(peer.ID(), packet.Types, packet.Sizes, packet.Hashes)

	case *eth.TransactionsPacket:
		for _, tx := range *packet {
			if tx.Type() == types.BlobTxType {
				return errors.New("disallowed broadcast blob transaction")
			}
		}
		return h.txFetcher.Enqueue(peer.ID(), *packet, false)

	case *eth.PooledTransactionsResponse:
		// If we receive any blob transactions missing sidecars, or with
		// sidecars that don't correspond to the versioned hashes reported
		// in the header, disconnect from the sending peer.
		for _, tx := range *packet {
			if tx.Type() == types.BlobTxType {
				if tx.BlobTxSidecar() == nil {
					return errors.New("received sidecar-less blob transaction")
				}
				if err := tx.BlobTxSidecar().ValidateBlobCommitmentHashes(tx.BlobHashes()); err != nil {
					return err
				}
			}
		}
		return h.txFetcher.Enqueue(peer.ID(), *packet, true)

	// XDC pre-merge block announcements and broadcasts
	case *eth.NewBlockHashesPacket:
		// XDC pre-merge: Log block hash announcements
		// These indicate the peer has new blocks we might need
		hashes := make([]string, len(*packet))
		for i, ann := range *packet {
			hashes[i] = fmt.Sprintf("%d:%s", ann.Number, ann.Hash.Hex()[:10])
		}
		peer.Log().Debug("XDC block hashes announced", "count", len(*packet), "blocks", hashes)
		// Update peer's head based on highest announced block
		// XDC fix: use block number as TD estimate instead of nil,
		// so bestPeer() can properly compare peer progress
		var highestNum uint64
		highestHash, _ := peer.Head()
		for _, ann := range *packet {
			if ann.Number > highestNum {
				highestNum = ann.Number
				highestHash = ann.Hash
			}
		}
		if highestNum > 0 {
			estimatedTd := new(big.Int).SetUint64(highestNum)
			peer.SetHead(highestHash, estimatedTd)
			// Notify syncer that peer has new blocks — reset backoffs
			if (*handler)(h).xdcSyncer != nil {
				(*handler)(h).xdcSyncer.resetAllBackoffs()
			}
		}
		return nil

	case *eth.NewBlockPacket:
		// XDC pre-merge: Try to import broadcast blocks
		block := packet.Block
		// Update peer's head (always, for peer tracking). resetAllBackoffs lets
		// the next forceSync tick pick this peer up immediately if it's now ahead.
		peer.SetHead(block.Hash(), packet.TD)
		if (*handler)(h).xdcSyncer != nil {
			(*handler)(h).xdcSyncer.resetAllBackoffs()
		}

		// Skip blocks far ahead of current head to avoid BAD BLOCK spam
		// that stalls the sync pipeline (Issue #18)
		currentHead := h.chain.CurrentBlock()
		if currentHead != nil && block.NumberU64() > currentHead.Number.Uint64()+1000 {
			// Silently drop — too far ahead to validate, not a bad block
			return nil
		}

		// Geth-aligned guard. Three reject cases, all deferred to the syncer:
		//
		// 1. Block ahead of head + 1. Parent unknown to InsertChain — would
		//    emit a permanent BAD BLOCK record (#577).
		//
		// 2. Block at head + 1 but parent hash doesn't match our head hash.
		//    Same problem; InsertChain reports BAD BLOCK.
		//
		// 3. Block N ≤ head − 32 (deep-reorg trigger). A peer broadcasting
		//    a single old block on a competing V2 branch will, if accepted
		//    here, cause InsertChain to fire a chain reorg that rewinds the
		//    canonical head by hundreds or thousands of blocks. Observed
		//    live on Apothem: peer sent block 81,874,965 while head was at
		//    81,876,204; V2 reorg dropped head 1239 blocks, then full sync
		//    re-fetched the whole range. Single-block propagation should
		//    never produce a deep reorg — that's a sync concern, not a
		//    propagation concern. The deepReorgGuard caps it.
		const deepReorgGuard = 32
		if currentHead != nil && block.NumberU64() > currentHead.Number.Uint64()+1 {
			peer.Log().Debug("XDC block received ahead of head, deferring to syncer",
				"number", block.NumberU64(), "head", currentHead.Number.Uint64())
			return nil
		}
		if currentHead != nil && currentHead.Number.Uint64() > deepReorgGuard &&
			block.NumberU64() < currentHead.Number.Uint64()-deepReorgGuard {
			peer.Log().Debug("XDC block received too far behind head, deferring to syncer (deep-reorg guard)",
				"number", block.NumberU64(), "head", currentHead.Number.Uint64(),
				"depth", currentHead.Number.Uint64()-block.NumberU64())
			return nil
		}
		if !h.chain.HasBlock(block.ParentHash(), block.NumberU64()-1) {
			peer.Log().Debug("XDC block received with unknown parent, deferring to syncer",
				"number", block.NumberU64(), "parent", block.ParentHash().Hex()[:10])
			return nil
		}

		peer.Log().Info("XDC block received", "number", block.NumberU64(), "hash", block.Hash().Hex()[:10])
		if _, err := h.chain.InsertChain([]*types.Block{block}); err != nil {
			peer.Log().Debug("XDC block import failed", "number", block.NumberU64(), "err", err)
		}
		return nil

	// XDC legacy sync: headers arrived in response to our request
	case *eth.BlockHeadersRequest:
		headers := []*types.Header(*packet)
		if len(headers) == 0 {
			peer.Log().Debug("XDC: empty headers response — delivering to signal end-of-range")
			// IMPORTANT: Deliver empty response so downloader knows peer has no more headers
			// instead of letting the 10s timeout fire. This eliminates timeout-induced gaps.
			(*handler)(h).downloader.DeliverHeadersXDC(peer.ID(), headers, peer.LastXDCHeaderReqID())
			return nil
		}
		peer.Log().Trace("Received headers from legacy response", "count", len(headers), "first", headers[0].Number.Uint64())
		
		// Deliver headers to the XDC downloader
		(*handler)(h).downloader.DeliverHeadersXDC(peer.ID(), headers, peer.LastXDCHeaderReqID())
		return nil

	// XDC legacy sync: bodies arrived in response to our request
	case *eth.BlockBodiesResponse:
		txs, uncles, _ := packet.Unpack()
		if len(txs) == 0 {
			peer.Log().Debug("XDC: empty bodies response")
			// Notify downloader so it can expire the in-flight request immediately
			// instead of waiting for the peer timeout to fire.
			(*handler)(h).downloader.DeliverBodiesXDC(peer.ID(), nil, nil, peer.LastXDCBodyReqID())
			return nil
		}
		peer.Log().Trace("Received bodies from legacy response", "count", len(txs))
		
		// Deliver bodies to the XDC downloader
		(*handler)(h).downloader.DeliverBodiesXDC(peer.ID(), txs, uncles, peer.LastXDCBodyReqID())
		return nil

	// XDPoS2 consensus messages — dispatch into V2 engine via bft.Bfter.
	case *eth.VotePacket: return (*handler)(h).handleInboundVote(peer, packet)
	case *eth.TimeoutPacket: return (*handler)(h).handleInboundTimeout(peer, packet)
	case *eth.SyncInfoPacket: return (*handler)(h).handleInboundSyncInfo(peer, packet)

	default:
		return fmt.Errorf("unexpected eth packet type: %T", packet)
	}
}
