package validator

import (
	"bytes"
	"context"
	"fmt"
	"math/big"
	"time"

	"github.com/OffchainLabs/prysm/v6/api/client/builder"
	"github.com/OffchainLabs/prysm/v6/beacon-chain/core/signing"
	fieldparams "github.com/OffchainLabs/prysm/v6/config/fieldparams"
	"github.com/OffchainLabs/prysm/v6/config/params"
	"github.com/OffchainLabs/prysm/v6/consensus-types/blocks"
	"github.com/OffchainLabs/prysm/v6/consensus-types/interfaces"
	"github.com/OffchainLabs/prysm/v6/consensus-types/primitives"
	"github.com/OffchainLabs/prysm/v6/encoding/bytesutil"
	"github.com/OffchainLabs/prysm/v6/encoding/ssz"
	"github.com/OffchainLabs/prysm/v6/monitoring/tracing"
	"github.com/OffchainLabs/prysm/v6/monitoring/tracing/trace"
	enginev1 "github.com/OffchainLabs/prysm/v6/proto/engine/v1"
	"github.com/OffchainLabs/prysm/v6/runtime/version"
	"github.com/OffchainLabs/prysm/v6/time/slots"
	"github.com/pkg/errors"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
	"github.com/sirupsen/logrus"
)

var (
	builderValueGweiGauge = promauto.NewGauge(prometheus.GaugeOpts{
		Name: "builder_value_gwei",
		Help: "Builder payload value in gwei",
	})
	localValueGweiGauge = promauto.NewGauge(prometheus.GaugeOpts{
		Name: "local_value_gwei",
		Help: "Local payload value in gwei",
	})
	builderGetPayloadMissCount = promauto.NewCounter(prometheus.CounterOpts{
		Name: "builder_get_payload_miss_count",
		Help: "The number of get payload misses for validator requests to builder",
	})
)

// emptyTransactionsRoot represents the returned value of ssz.TransactionsRoot([][]byte{}) and
// can be used as a constant to avoid recomputing this value in every call.
var emptyTransactionsRoot = [32]byte{127, 254, 36, 30, 166, 1, 135, 253, 176, 24, 123, 250, 34, 222, 53, 209, 249, 190, 215, 171, 6, 29, 148, 1, 253, 71, 227, 74, 84, 251, 237, 225}

// blockBuilderTimeout is the maximum amount of time allowed for a block builder to respond to a
// block request. This value is known as `BUILDER_PROPOSAL_DELAY_TOLERANCE` in builder spec.
const blockBuilderTimeout = 1 * time.Second
const gasLimitAdjustmentFactor = 1024

// Sets the execution data for the block. Execution data can come from local EL client or remote builder depends on validator registration and circuit breaker conditions.
func setExecutionData(ctx context.Context, blk interfaces.SignedBeaconBlock, local *blocks.GetPayloadResponse, bid builder.Bid, builderBoostFactor primitives.Gwei) (primitives.Wei, enginev1.BlobsBundler, error) {
	_, span := trace.StartSpan(ctx, "ProposerServer.setExecutionData")
	defer span.End()

	slot := blk.Block().Slot()
	if slots.ToEpoch(slot) < params.BeaconConfig().BellatrixForkEpoch {
		return primitives.ZeroWei(), nil, nil
	}

	if local == nil {
		return primitives.ZeroWei(), nil, errors.New("local payload is nil")
	}

	// Use local payload if builder payload is nil.
	if bid == nil {
		return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
	}

	builderPayload, err := bid.Header()
	if err != nil {
		log.WithError(err).Warn("Proposer: failed to retrieve header from BuilderBid")
		return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
	}

	switch {
	case blk.Version() >= version.Capella:
		withdrawalsMatched, err := matchingWithdrawalsRoot(local.ExecutionData, builderPayload)
		if err != nil {
			tracing.AnnotateError(span, err)
			log.WithError(err).Warn("Proposer: failed to match withdrawals root")
			return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
		}

		// Compare payload values between local and builder. Default to the local value if it is higher.
		localValueGwei := primitives.WeiToGwei(local.Bid)
		builderValueGwei := primitives.WeiToGwei(bid.Value())
		minBid := primitives.Gwei(params.BeaconConfig().MinBuilderBid)
		// Use local block if min bid is not attained
		if builderValueGwei < minBid {
			log.WithFields(logrus.Fields{
				"minBuilderBid":    minBid,
				"builderGweiValue": builderValueGwei,
			}).Warn("Proposer: using local execution payload because min bid not attained")
			return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
		}

		// Use local block if min difference is not attained
		minDiff := localValueGwei + primitives.Gwei(params.BeaconConfig().MinBuilderDiff)
		if builderValueGwei < minDiff {
			log.WithFields(logrus.Fields{
				"localGweiValue":   localValueGwei,
				"minBidDiff":       minDiff,
				"builderGweiValue": builderValueGwei,
			}).Warn("Proposer: using local execution payload because min difference with local value was not attained")
			return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
		}

		// Use builder payload if the following in true:
		// builder_bid_value * builderBoostFactor(default 100) > local_block_value * (local-block-value-boost + 100)
		boost := primitives.Gwei(params.BeaconConfig().LocalBlockValueBoost)
		higherValueBuilder := builderValueGwei*builderBoostFactor > localValueGwei*(100+boost)
		if boost > 0 && builderBoostFactor != defaultBuilderBoostFactor {
			log.WithFields(logrus.Fields{
				"localGweiValue":       localValueGwei,
				"localBoostPercentage": boost,
				"builderGweiValue":     builderValueGwei,
				"builderBoostFactor":   builderBoostFactor,
			}).Warn("Proposer: both local boost and builder boost are using non default values")
		}
		builderValueGweiGauge.Set(float64(builderValueGwei))
		localValueGweiGauge.Set(float64(localValueGwei))

		// If we can't get the builder value, just use local block.
		if higherValueBuilder && withdrawalsMatched { // Builder value is higher and withdrawals match.
			var builderKzgCommitments [][]byte
			if bid.Version() >= version.Deneb {
				bidDeneb, ok := bid.(builder.BidDeneb)
				if !ok {
					log.Warnf("Bid type %T does not implement builder.BidDeneb", bid)
					return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
				} else {
					builderKzgCommitments = bidDeneb.BlobKzgCommitments()
				}
			}

			var executionRequests *enginev1.ExecutionRequests
			if bid.Version() >= version.Electra {
				bidElectra, ok := bid.(builder.BidElectra)
				if !ok {
					log.Warnf("Bid type %T does not implement builder.BidElectra", bid)
					return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
				} else {
					executionRequests = bidElectra.ExecutionRequests()
				}
			}
			if err := setBuilderExecution(blk, builderPayload, builderKzgCommitments, executionRequests); err != nil {
				log.WithError(err).Warn("Proposer: failed to set builder payload")
				return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
			} else {
				return bid.Value(), nil, nil
			}
		}
		if !higherValueBuilder {
			log.WithFields(logrus.Fields{
				"localGweiValue":       localValueGwei,
				"localBoostPercentage": boost,
				"builderGweiValue":     builderValueGwei,
				"builderBoostFactor":   builderBoostFactor,
			}).Warn("Proposer: using local execution payload because higher value")
		}
		span.SetAttributes(
			trace.BoolAttribute("higherValueBuilder", higherValueBuilder),
			trace.Int64Attribute("localGweiValue", int64(localValueGwei)),         // lint:ignore uintcast -- This is OK for tracing.
			trace.Int64Attribute("localBoostPercentage", int64(boost)),            // lint:ignore uintcast -- This is OK for tracing.
			trace.Int64Attribute("builderGweiValue", int64(builderValueGwei)),     // lint:ignore uintcast -- This is OK for tracing.
			trace.Int64Attribute("builderBoostFactor", int64(builderBoostFactor)), // lint:ignore uintcast -- This is OK for tracing.
		)
		return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
	default: // Bellatrix case.
		if err := setBuilderExecution(blk, builderPayload, nil, nil); err != nil {
			log.WithError(err).Warn("Proposer: failed to set builder payload")
			return local.Bid, local.BlobsBundler, setLocalExecution(blk, local)
		} else {
			return bid.Value(), nil, nil
		}
	}
}

// This function retrieves the payload header and kzg commitments given the slot number and the validator index.
// It's a no-op if the latest head block is not versioned bellatrix.
func (vs *Server) getPayloadHeaderFromBuilder(
	ctx context.Context,
	slot primitives.Slot,
	idx primitives.ValidatorIndex,
	parentGasLimit uint64) (builder.Bid, error) {
	ctx, span := trace.StartSpan(ctx, "ProposerServer.getPayloadHeaderFromBuilder")
	defer span.End()

	if slots.ToEpoch(slot) < params.BeaconConfig().BellatrixForkEpoch {
		return nil, errors.New("can't get payload header from builder before bellatrix epoch")
	}

	b, err := vs.HeadFetcher.HeadBlock(ctx)
	if err != nil {
		return nil, err
	}

	h, err := b.Block().Body().Execution()
	if err != nil {
		return nil, errors.Wrap(err, "failed to get execution header")
	}
	pk, err := vs.HeadFetcher.HeadValidatorIndexToPublicKey(ctx, idx)
	if err != nil {
		return nil, err
	}

	ctx, cancel := context.WithTimeout(ctx, blockBuilderTimeout)
	defer cancel()

	signedBid, err := vs.BlockBuilder.GetHeader(ctx, slot, bytesutil.ToBytes32(h.BlockHash()), pk)
	if err != nil {
		return nil, err
	}
	if signedBid == nil || signedBid.IsNil() {
		return nil, errors.New("builder returned nil bid")
	}
	bidVersion := signedBid.Version()
	epoch := slots.ToEpoch(slot)
	entry := params.GetNetworkScheduleEntry(epoch)
	forkVersion := entry.VersionEnum
	if !isVersionCompatible(bidVersion, forkVersion) {
		return nil, fmt.Errorf("builder bid response version: %d is not compatible with expected version: %d for epoch %d", bidVersion, forkVersion, epoch)
	}

	bid, err := signedBid.Message()
	if err != nil {
		return nil, errors.Wrap(err, "could not get bid")
	}
	if bid == nil || bid.IsNil() {
		return nil, errors.New("builder returned nil bid")
	}

	v := bid.Value()
	if big.NewInt(0).Cmp(v) == 0 {
		return nil, errors.New("builder returned header with 0 bid amount")
	}

	header, err := bid.Header()
	if err != nil {
		return nil, errors.Wrap(err, "could not get bid header")
	}
	txRoot, err := header.TransactionsRoot()
	if err != nil {
		return nil, errors.Wrap(err, "could not get transaction root")
	}
	if bytesutil.ToBytes32(txRoot) == emptyTransactionsRoot {
		return nil, errors.New("builder returned header with an empty tx root")
	}

	if !bytes.Equal(header.ParentHash(), h.BlockHash()) {
		return nil, fmt.Errorf("incorrect parent hash %#x != %#x", header.ParentHash(), h.BlockHash())
	}

	reg, err := vs.BlockBuilder.RegistrationByValidatorID(ctx, idx)
	if err != nil {
		log.WithError(err).Warn("Proposer: failed to get registration by validator ID, could not check gas limit")
	} else {
		gasLimit := expectedGasLimit(parentGasLimit, reg.GasLimit)
		if gasLimit != header.GasLimit() {
			return nil, fmt.Errorf("incorrect header gas limit %d != %d", gasLimit, header.GasLimit())
		}
	}

	t, err := slots.StartTime(vs.TimeFetcher.GenesisTime(), slot)
	if err != nil {
		return nil, err
	}
	if header.Timestamp() != uint64(t.Unix()) {
		return nil, fmt.Errorf("incorrect timestamp %d != %d", header.Timestamp(), uint64(t.Unix()))
	}

	if err := validateBuilderSignature(signedBid); err != nil {
		return nil, errors.Wrap(err, "could not validate builder signature")
	}

	var kzgCommitments [][]byte
	if bid.Version() >= version.Deneb {
		dBid, ok := bid.(builder.BidDeneb)
		if !ok {
			return nil, fmt.Errorf("bid type %T does not implement builder.BidDeneb", dBid)
		}
		kzgCommitments = dBid.BlobKzgCommitments()
	}
	var executionRequests *enginev1.ExecutionRequests
	if bid.Version() >= version.Electra {
		eBid, ok := bid.(builder.BidElectra)
		if !ok {
			return nil, fmt.Errorf("bid type %T does not implement builder.BidElectra", eBid)
		}
		executionRequests = eBid.ExecutionRequests()
	}
	l := log.WithFields(logrus.Fields{
		"gweiValue":          primitives.WeiToGwei(v),
		"builderPubKey":      fmt.Sprintf("%#x", bid.Pubkey()),
		"blockHash":          fmt.Sprintf("%#x", header.BlockHash()),
		"slot":               slot,
		"validator":          idx,
		"sinceSlotStartTime": time.Since(t),
	})
	if len(kzgCommitments) > 0 {
		l = l.WithField("kzgCommitmentCount", len(kzgCommitments))
	}
	if executionRequests != nil {
		l = l.WithField("depositRequestCount", len(executionRequests.Deposits))
		l = l.WithField("withdrawalRequestCount", len(executionRequests.Withdrawals))
		l = l.WithField("consolidationRequestCount", len(executionRequests.Consolidations))
	}
	l.Info("Received header with bid")

	span.SetAttributes(
		trace.StringAttribute("value", primitives.WeiToBigInt(v).String()),
		trace.StringAttribute("builderPubKey", fmt.Sprintf("%#x", bid.Pubkey())),
		trace.StringAttribute("blockHash", fmt.Sprintf("%#x", header.BlockHash())),
	)

	return bid, nil
}

// Validates builder signature and returns an error if the signature is invalid.
func validateBuilderSignature(signedBid builder.SignedBid) error {
	d, err := signing.ComputeDomain(params.BeaconConfig().DomainApplicationBuilder,
		nil, /* fork version */
		nil /* genesis val root */)
	if err != nil {
		return err
	}
	if signedBid == nil || signedBid.IsNil() {
		return errors.New("nil builder bid")
	}
	bid, err := signedBid.Message()
	if err != nil {
		return errors.Wrap(err, "could not get bid")
	}
	if bid == nil || bid.IsNil() {
		return errors.New("builder returned nil bid")
	}
	return signing.VerifySigningRoot(bid, bid.Pubkey(), signedBid.Signature(), d)
}

func matchingWithdrawalsRoot(local, builder interfaces.ExecutionData) (bool, error) {
	wds, err := local.Withdrawals()
	if err != nil {
		return false, errors.Wrap(err, "could not get local withdrawals")
	}
	br, err := builder.WithdrawalsRoot()
	if err != nil {
		return false, errors.Wrap(err, "could not get builder withdrawals root")
	}
	wr, err := ssz.WithdrawalSliceRoot(wds, fieldparams.MaxWithdrawalsPerPayload)
	if err != nil {
		return false, errors.Wrap(err, "could not compute local withdrawals root")
	}

	if !bytes.Equal(br, wr[:]) {
		log.WithFields(logrus.Fields{
			"local":   fmt.Sprintf("%#x", wr),
			"builder": fmt.Sprintf("%#x", br),
		}).Warn("Proposer: withdrawal roots don't match, using local block")
		return false, nil
	}
	return true, nil
}

// setLocalExecution sets the execution context for a local beacon block.
// It delegates to setExecution for the actual work.
func setLocalExecution(blk interfaces.SignedBeaconBlock, local *blocks.GetPayloadResponse) error {
	var kzgCommitments [][]byte
	if local.BlobsBundler != nil {
		kzgCommitments = local.BlobsBundler.GetKzgCommitments()
	}
	if local.ExecutionRequests != nil {
		if err := blk.SetExecutionRequests(local.ExecutionRequests); err != nil {
			return errors.Wrap(err, "could not set execution requests")
		}
	}
	return setExecution(blk, local.ExecutionData, false, kzgCommitments, local.ExecutionRequests)
}

// setBuilderExecution sets the execution context for a builder's beacon block.
// It delegates to setExecution for the actual work.
func setBuilderExecution(blk interfaces.SignedBeaconBlock, execution interfaces.ExecutionData, builderKzgCommitments [][]byte, requests *enginev1.ExecutionRequests) error {
	return setExecution(blk, execution, true, builderKzgCommitments, requests)
}

// setExecution sets the execution context for a beacon block. It also sets KZG commitments based on the block version.
// The function is designed to be flexible and handle both local and builder executions.
func setExecution(blk interfaces.SignedBeaconBlock, execution interfaces.ExecutionData, isBlinded bool, kzgCommitments [][]byte, requests *enginev1.ExecutionRequests) error {
	if execution == nil {
		return errors.New("execution is nil")
	}

	// Set the execution data for the block
	errMessage := "failed to set local execution"
	if isBlinded {
		errMessage = "failed to set builder execution"
	}
	if err := blk.SetExecution(execution); err != nil {
		return errors.Wrap(err, errMessage)
	}

	// If the block version is below Deneb, no further actions are needed
	if blk.Version() < version.Deneb {
		return nil
	}

	// Set the KZG commitments for the block
	kzgErr := "failed to set local kzg commitments"
	if isBlinded {
		kzgErr = "failed to set builder kzg commitments"
	}
	if err := blk.SetBlobKzgCommitments(kzgCommitments); err != nil {
		return errors.Wrap(err, kzgErr)
	}

	// If the block version is below Electra, no further actions are needed
	if blk.Version() < version.Electra {
		return nil
	}

	// Set the execution requests
	requestsErr := "failed to set local execution requests"
	if isBlinded {
		requestsErr = "failed to set builder execution requests"
	}
	if err := blk.SetExecutionRequests(requests); err != nil {
		return errors.Wrap(err, requestsErr)
	}
	return nil
}

// Calculates expected gas limit based on parent gas limit and target gas limit.
// Spec code:
//
//	def expected_gas_limit(parent_gas_limit, target_gas_limit, adjustment_factor):
//	 max_gas_limit_difference = (parent_gas_limit // adjustment_factor) - 1
//	 if target_gas_limit > parent_gas_limit:
//	     gas_diff = target_gas_limit - parent_gas_limit
//	     return parent_gas_limit + min(gas_diff, max_gas_limit_difference)
//	 else:
//	     gas_diff = parent_gas_limit - target_gas_limit
//	     return parent_gas_limit - min(gas_diff, max_gas_limit_difference)
func expectedGasLimit(parentGasLimit, proposerGasLimit uint64) uint64 {
	maxGasLimitDiff := uint64(0)
	if parentGasLimit > gasLimitAdjustmentFactor {
		maxGasLimitDiff = parentGasLimit/gasLimitAdjustmentFactor - 1
	}
	if proposerGasLimit > parentGasLimit {
		if proposerGasLimit-parentGasLimit > maxGasLimitDiff {
			return parentGasLimit + maxGasLimitDiff
		}
		return proposerGasLimit
	}

	if parentGasLimit-proposerGasLimit > maxGasLimitDiff {
		return parentGasLimit - maxGasLimitDiff
	}
	return proposerGasLimit
}

// isVersionCompatible checks if a builder bid version is compatible with the head block version.
func isVersionCompatible(bidVersion, headBlockVersion int) bool {
	// Exact version match is always compatible
	if bidVersion == headBlockVersion {
		return true
	}

	// Allow Electra bids for Fulu blocks - they have compatible payload formats
	if bidVersion == version.Electra && headBlockVersion == version.Fulu {
		return true
	}

	// For all other cases, require exact version match
	return false
}
