package sync

import (
	"context"
	"fmt"
	"sort"
	"time"

	"github.com/OffchainLabs/prysm/v7/beacon-chain/db"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p/types"
	"github.com/OffchainLabs/prysm/v7/cmd/beacon-chain/flags"
	fieldparams "github.com/OffchainLabs/prysm/v7/config/fieldparams"
	"github.com/OffchainLabs/prysm/v7/config/params"
	"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
	"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
	"github.com/OffchainLabs/prysm/v7/monitoring/tracing"
	"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
	"github.com/OffchainLabs/prysm/v7/time/slots"
	libp2pcore "github.com/libp2p/go-libp2p/core"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

// blobSidecarByRootRPCHandler handles the /eth2/beacon_chain/req/blob_sidecars_by_root/1/ RPC request.
// spec: https://github.com/ethereum/consensus-specs/blob/a7e45db9ac2b60a33e144444969ad3ac0aae3d4c/specs/deneb/p2p-interface.md#blobsidecarsbyroot-v1
func (s *Service) blobSidecarByRootRPCHandler(ctx context.Context, msg any, stream libp2pcore.Stream) error {
	ctx, span := trace.StartSpan(ctx, "sync.blobSidecarByRootRPCHandler")
	defer span.End()
	ctx, cancel := context.WithTimeout(ctx, ttfbTimeout)
	defer cancel()
	SetRPCStreamDeadlines(stream)
	log := log.WithField("handler", p2p.BlobSidecarsByRootName[1:]) // slice the leading slash off the name var
	ref, ok := msg.(*types.BlobSidecarsByRootReq)
	if !ok {
		return errors.New("message is not type BlobSidecarsByRootReq")
	}

	blobIdents := *ref

	if err := s.rateLimiter.validateRequest(stream, uint64(len(blobIdents))); err != nil {
		return errors.Wrap(err, "rate limiter validate request")
	}

	cs := s.cfg.clock.CurrentSlot()
	remotePeer := stream.Conn().RemotePeer()
	if err := validateBlobByRootRequest(blobIdents, cs); err != nil {
		s.downscorePeer(remotePeer, "blobSidecarsByRootRpcHandlerValidationError")
		s.writeErrorResponseToStream(responseCodeInvalidRequest, err.Error(), stream)
		return err
	}

	// Sort the identifiers so that requests for the same blob root will be adjacent, minimizing db lookups.
	sort.Sort(blobIdents)

	batchSize := flags.Get().BlobBatchLimit
	var ticker *time.Ticker
	if len(blobIdents) > batchSize {
		ticker = time.NewTicker(blobRpcThrottleInterval)
	}

	// Compute the oldest slot we'll allow a peer to request, based on the current slot.
	minReqSlot, err := BlobRPCMinValidSlot(cs)
	if err != nil {
		return errors.Wrapf(err, "unexpected error computing min valid blob request slot, current_slot=%d", cs)
	}

	// Extract all needed roots.
	roots := make([][fieldparams.RootLength]byte, 0, len(blobIdents))
	for _, ident := range blobIdents {
		root := bytesutil.ToBytes32(ident.BlockRoot)
		roots = append(roots, root)
	}

	// Filter all available roots in block storage.
	availableRoots := s.cfg.beaconDB.AvailableBlocks(ctx, roots)

	// Serve each requested blob sidecar.
	for i := range blobIdents {
		if err := ctx.Err(); err != nil {
			closeStream(stream, log)
			return err
		}

		// Throttle request processing to no more than batchSize/sec.
		if i != 0 && i%batchSize == 0 && ticker != nil {
			<-ticker.C
		}
		s.rateLimiter.add(stream, 1)

		root, idx := bytesutil.ToBytes32(blobIdents[i].BlockRoot), blobIdents[i].Index

		// Do not serve a blob sidecar if the corresponding block is not available.
		if !availableRoots[root] {
			log.Trace("Peer requested blob sidecar by root but corresponding block not found in db")
			continue
		}

		sc, err := s.cfg.blobStorage.Get(root, idx)
		if err != nil {
			log := log.WithFields(logrus.Fields{
				"root":  fmt.Sprintf("%#x", root),
				"index": idx,
				"peer":  remotePeer.String(),
			})

			if db.IsNotFound(err) {
				log.Trace("Peer requested blob sidecar by root not found in db")
				continue
			}

			log.Error("Unexpected DB error retrieving blob sidecar from storage")
			s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream)

			return errors.Wrap(err, "get blob sidecar by root")
		}

		// If any root in the request content references a block earlier than minimum_request_epoch,
		// peers MAY respond with error code 3: ResourceUnavailable or not include the blob in the response.
		// note: we are deviating from the spec to allow requests for blobs that are before minimum_request_epoch,
		// up to the beginning of the retention period.
		if sc.Slot() < minReqSlot {
			s.writeErrorResponseToStream(responseCodeResourceUnavailable, types.ErrBlobLTMinRequest.Error(), stream)
			log.WithError(types.ErrBlobLTMinRequest).
				Debugf("Requested blob for block %#x before minimum_request_epoch", blobIdents[i].BlockRoot)
			return types.ErrBlobLTMinRequest
		}

		SetStreamWriteDeadline(stream, defaultWriteDuration)
		if chunkErr := WriteBlobSidecarChunk(stream, s.cfg.clock, s.cfg.p2p.Encoding(), sc); chunkErr != nil {
			log.WithError(chunkErr).Debug("Could not send a chunked response")
			s.writeErrorResponseToStream(responseCodeServerError, types.ErrGeneric.Error(), stream)
			tracing.AnnotateError(span, chunkErr)
			return chunkErr
		}
	}
	closeStream(stream, log)
	return nil
}

func validateBlobByRootRequest(blobIdents types.BlobSidecarsByRootReq, slot primitives.Slot) error {
	cfg := params.BeaconConfig()
	epoch := slots.ToEpoch(slot)
	blobIdentCount := uint64(len(blobIdents))

	if epoch >= cfg.ElectraForkEpoch {
		if blobIdentCount > cfg.MaxRequestBlobSidecarsElectra {
			return types.ErrMaxBlobReqExceeded
		}

		return nil
	}

	if blobIdentCount > cfg.MaxRequestBlobSidecars {
		return types.ErrMaxBlobReqExceeded
	}

	return nil
}
