package sync

import (
	"context"
	"fmt"
	"os"
	"path"
	"strings"

	"github.com/OffchainLabs/prysm/v7/beacon-chain/p2p"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/verification"
	"github.com/OffchainLabs/prysm/v7/config/features"
	"github.com/OffchainLabs/prysm/v7/config/params"
	"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
	"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
	"github.com/OffchainLabs/prysm/v7/crypto/rand"
	"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
	"github.com/OffchainLabs/prysm/v7/io/file"
	eth "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
	prysmTime "github.com/OffchainLabs/prysm/v7/time"
	"github.com/OffchainLabs/prysm/v7/time/slots"
	pubsub "github.com/libp2p/go-libp2p-pubsub"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

func (s *Service) validateBlob(ctx context.Context, pid peer.ID, msg *pubsub.Message) (pubsub.ValidationResult, error) {
	receivedTime := prysmTime.Now()

	if pid == s.cfg.p2p.PeerID() {
		return pubsub.ValidationAccept, nil
	}
	if s.cfg.initialSync.Syncing() {
		return pubsub.ValidationIgnore, nil
	}
	if msg.Topic == nil {
		return pubsub.ValidationReject, p2p.ErrInvalidTopic
	}
	m, err := s.decodePubsubMessage(msg)
	if err != nil {
		log.WithError(err).Error("Failed to decode message")
		return pubsub.ValidationReject, err
	}

	bpb, ok := m.(*eth.BlobSidecar)
	if !ok {
		log.WithField("message", m).Error("Message is not of type *eth.BlobSidecar")
		return pubsub.ValidationReject, errWrongMessage
	}
	blob, err := blocks.NewROBlob(bpb)
	if err != nil {
		return pubsub.ValidationReject, errors.Wrap(err, "roblob conversion failure")
	}
	vf := s.newBlobVerifier(blob, verification.GossipBlobSidecarRequirements)

	if err := vf.BlobIndexInBounds(); err != nil {
		return pubsub.ValidationReject, err
	}

	// [REJECT] The sidecar is for the correct subnet -- i.e. compute_subnet_for_blob_sidecar(sidecar.index) == subnet_id.
	want := fmt.Sprintf("blob_sidecar_%d", computeSubnetForBlobSidecar(blob.Index, blob.Slot()))
	if !strings.Contains(*msg.Topic, want) {
		log.WithFields(blobFields(blob)).Debug("Sidecar index does not match topic")
		return pubsub.ValidationReject, fmt.Errorf("wrong topic name: %s", *msg.Topic)
	}

	if err := vf.NotFromFutureSlot(); err != nil {
		return pubsub.ValidationIgnore, err
	}

	startTime, err := slots.StartTime(s.cfg.clock.GenesisTime(), blob.Slot())
	if err != nil {
		return pubsub.ValidationIgnore, err
	}

	// [IGNORE] The sidecar is the first sidecar for the tuple (block_header.slot, block_header.proposer_index, sidecar.index) with valid header signature and sidecar inclusion proof
	if s.hasSeenBlobIndex(blob.Slot(), blob.ProposerIndex(), blob.Index) {
		return pubsub.ValidationIgnore, nil
	}

	if err := vf.SlotAboveFinalized(); err != nil {
		return pubsub.ValidationIgnore, err
	}

	if err := vf.SidecarParentSeen(s.hasBadBlock); err != nil {
		go func() {
			if err := s.sendBatchRootRequest(context.Background(), [][32]byte{blob.ParentRoot()}, rand.NewGenerator()); err != nil {
				log.WithError(err).WithFields(blobFields(blob)).Debug("Failed to send batch root request")
			}
		}()
		missingParentBlobSidecarCount.Inc()
		return pubsub.ValidationIgnore, err
	}

	if err := vf.SidecarParentValid(s.hasBadBlock); err != nil {
		return pubsub.ValidationReject, err
	}

	if err := vf.ValidProposerSignature(ctx); err != nil {
		return pubsub.ValidationReject, err
	}

	if err := vf.SidecarParentSlotLower(); err != nil {
		return pubsub.ValidationReject, err
	}

	if err := vf.SidecarDescendsFromFinalized(); err != nil {
		return pubsub.ValidationReject, err
	}

	if err := vf.SidecarInclusionProven(); err != nil {
		return pubsub.ValidationReject, err
	}

	if err := vf.SidecarKzgProofVerified(); err != nil {
		saveInvalidBlobToTemp(blob)
		return pubsub.ValidationReject, err
	}

	if err := vf.SidecarProposerExpected(ctx); err != nil {
		return pubsub.ValidationReject, err
	}

	fields := blobFields(blob)
	sinceSlotStartTime := receivedTime.Sub(startTime)
	validationTime := s.cfg.clock.Now().Sub(receivedTime)
	fields["sinceSlotStartTime"] = sinceSlotStartTime
	fields["validationTime"] = validationTime
	log.WithFields(fields).Debug("Received blob sidecar gossip")

	blobSidecarVerificationGossipSummary.Observe(float64(validationTime.Milliseconds()))
	blobSidecarArrivalGossipSummary.Observe(float64(sinceSlotStartTime.Milliseconds()))

	vBlobData, err := vf.VerifiedROBlob()
	if err != nil {
		return pubsub.ValidationReject, err
	}
	msg.ValidatorData = vBlobData

	return pubsub.ValidationAccept, nil
}

// Returns true if the blob with the same slot, proposer index, and blob index has been seen before.
func (s *Service) hasSeenBlobIndex(slot primitives.Slot, proposerIndex primitives.ValidatorIndex, index uint64) bool {
	s.seenBlobLock.RLock()
	defer s.seenBlobLock.RUnlock()
	b := append(bytesutil.Bytes32(uint64(slot)), bytesutil.Bytes32(uint64(proposerIndex))...)
	b = append(b, bytesutil.Bytes32(index)...)
	_, seen := s.seenBlobCache.Get(string(b))
	return seen
}

// Sets the blob with the same slot, proposer index, and blob index as seen.
func (s *Service) setSeenBlobIndex(slot primitives.Slot, proposerIndex primitives.ValidatorIndex, index uint64) {
	s.seenBlobLock.Lock()
	defer s.seenBlobLock.Unlock()
	b := append(bytesutil.Bytes32(uint64(slot)), bytesutil.Bytes32(uint64(proposerIndex))...)
	b = append(b, bytesutil.Bytes32(index)...)
	s.seenBlobCache.Add(string(b), true)
}

func blobFields(b blocks.ROBlob) logrus.Fields {
	return logrus.Fields{
		"slot":          b.Slot(),
		"proposerIndex": b.ProposerIndex(),
		"blockRoot":     fmt.Sprintf("%#x", b.BlockRoot()),
		"kzgCommitment": fmt.Sprintf("%#x", b.KzgCommitment),
		"index":         b.Index,
	}
}

func computeSubnetForBlobSidecar(index uint64, slot primitives.Slot) uint64 {
	subnetCount := params.BeaconConfig().BlobsidecarSubnetCount
	if slots.ToEpoch(slot) >= params.BeaconConfig().ElectraForkEpoch {
		subnetCount = params.BeaconConfig().BlobsidecarSubnetCountElectra
	}
	return index % subnetCount
}

// saveInvalidBlobToTemp as a block ssz. Writes to temp directory.
func saveInvalidBlobToTemp(b blocks.ROBlob) {
	if !features.Get().SaveInvalidBlob {
		return
	}
	filename := fmt.Sprintf("blob_sidecar_%#x_%d_%d.ssz", b.BlockRoot(), b.Slot(), b.Index)
	fp := path.Join(os.TempDir(), filename)
	log.Warnf("Writing invalid blob sidecar to disk at %s", fp)
	enc, err := b.MarshalSSZ()
	if err != nil {
		log.WithError(err).Error("Failed to ssz encode blob sidecar")
		return
	}
	if err := file.WriteFile(fp, enc); err != nil {
		log.WithError(err).Error("Failed to write to disk")
	}
}
