// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon 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.
//
// Erigon 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 Erigon. If not, see <http://www.gnu.org/licenses/>.

package p2p

import (
	"context"
	"errors"
	"fmt"

	"google.golang.org/grpc"
	"google.golang.org/protobuf/types/known/emptypb"

	"github.com/erigontech/erigon/execution/rlp"
	"github.com/erigontech/erigon/node/direct"
	"github.com/erigontech/erigon/node/gointerfaces/sentryproto"
	"github.com/erigontech/erigon/p2p/protocols/eth"
	"github.com/erigontech/erigon/p2p/sentry/libsentry"
)

var ErrPeerNotFound = errors.New("peer not found")

func NewMessageSender(sentryClient sentryproto.SentryClient) *MessageSender {
	return &MessageSender{
		sentryClient: sentryClient,
	}
}

type MessageSender struct {
	sentryClient sentryproto.SentryClient
}

func (ms *MessageSender) SendGetBlockHeaders(ctx context.Context, peerId *PeerId, req eth.GetBlockHeadersPacket66) error {
	return ms.sendMessageToPeer(ctx, sentryproto.MessageId_GET_BLOCK_HEADERS_66, req, peerId)
}

func (ms *MessageSender) SendGetBlockBodies(ctx context.Context, peerId *PeerId, req eth.GetBlockBodiesPacket66) error {
	return ms.sendMessageToPeer(ctx, sentryproto.MessageId_GET_BLOCK_BODIES_66, req, peerId)
}

func (ms *MessageSender) SendNewBlockHashes(ctx context.Context, peerId *PeerId, req eth.NewBlockHashesPacket) error {
	return ms.sendMessageToPeer(ctx, sentryproto.MessageId_NEW_BLOCK_HASHES_66, req, peerId)
}

func (ms *MessageSender) SendNewBlock(ctx context.Context, peerId *PeerId, req eth.NewBlockPacket) error {
	return ms.sendMessageToPeer(ctx, sentryproto.MessageId_NEW_BLOCK_66, req, peerId)
}

func (ms *MessageSender) SendBlockRangeUpdate(ctx context.Context, req eth.BlockRangeUpdatePacket) error {
	return ms.sendMessageAllPeers(ctx, sentryproto.MessageId_BLOCK_RANGE_UPDATE_69, req)
}

func (ms *MessageSender) SupportsBlockRangeUpdate(ctx context.Context) (bool, error) {
	reply, err := ms.sentryClient.HandShake(ctx, &emptypb.Empty{}, grpc.WaitForReady(true))
	if err != nil {
		return false, err
	}
	version, ok := direct.ProtocolToUintMap[reply.Protocol]
	if !ok {
		return false, fmt.Errorf("unknown sentry protocol %q", reply.Protocol.String())
	}
	return version >= direct.ETH69, nil
}

func (ms *MessageSender) sendMessageToPeer(ctx context.Context, messageId sentryproto.MessageId, data any, peerId *PeerId) error {
	rlpData, err := rlp.EncodeToBytes(data)
	if err != nil {
		return err
	}

	sent, err := ms.sentryClient.SendMessageById(ctx, &sentryproto.SendMessageByIdRequest{
		PeerId: peerId.H512(),
		Data: &sentryproto.OutboundMessageData{
			Id:   messageId,
			Data: rlpData,
		},
	})
	if err != nil {
		if libsentry.IsPeerNotFoundErr(err) {
			return fmt.Errorf("%w: %s", ErrPeerNotFound, peerId.String())
		}
		return err
	}
	if len(sent.Peers) == 0 {
		return fmt.Errorf("%w: %s", ErrPeerNotFound, peerId.String())
	}

	return nil
}

func (ms *MessageSender) sendMessageAllPeers(ctx context.Context, messageId sentryproto.MessageId, data any) error {
	rlpData, err := rlp.EncodeToBytes(data)
	if err != nil {
		return err
	}

	_, err = ms.sentryClient.SendMessageToAll(ctx, &sentryproto.OutboundMessageData{
		Id:   messageId,
		Data: rlpData,
	})

	return err
}
