// 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 types

import (
	"errors"
	"fmt"
	"io"

	"github.com/holiman/uint256"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/crypto/kzg"
	"github.com/erigontech/erigon/common/length"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/execution/protocol/params"
	"github.com/erigontech/erigon/execution/rlp"
	"github.com/erigontech/erigon/execution/types/accounts"
)

var (
	ErrNilToFieldTx                = errors.New("txn: field 'To' can not be 'nil'")
	ErrBlobTxnEmptyBlobs           = errors.New("blob txn must contain at least one blob versioned hash")
	ErrBlobTxnInvalidVersionedHash = errors.New("blob txn versioned hash has invalid version byte")
)

type BlobTx struct {
	DynamicFeeTransaction
	MaxFeePerBlobGas    uint256.Int
	BlobVersionedHashes []common.Hash
}

func (stx *BlobTx) Type() byte { return BlobTxType }

// copyData returns a copy of BlobTx where the TransactionMisc cache fields
// (hash, from) are not copied directly but rebuilt field-by-field, avoiding
// go vet copylocks warnings on the embedded sync/atomic.Pointer.
func (stx *BlobTx) copyData() BlobTx {
	return BlobTx{
		DynamicFeeTransaction: DynamicFeeTransaction{
			CommonTx:   stx.CommonTx.copyData(),
			ChainID:    stx.ChainID,
			TipCap:     stx.TipCap,
			FeeCap:     stx.FeeCap,
			AccessList: stx.AccessList,
		},
		MaxFeePerBlobGas:    stx.MaxFeePerBlobGas,
		BlobVersionedHashes: stx.BlobVersionedHashes,
	}
}

func (stx *BlobTx) GetBlobHashes() []common.Hash {
	return stx.BlobVersionedHashes
}

func (stx *BlobTx) GetBlobGas() uint64 {
	return params.GasPerBlob * uint64(len(stx.BlobVersionedHashes))
}

func (stx *BlobTx) AsMessage(s Signer, baseFee *uint256.Int, rules *chain.Rules) (*Message, error) {
	if !rules.IsCancun {
		return nil, errors.New("BlobTx transactions require Cancun")
	}
	// EIP-4844 transaction validity: a blob txn must specify a recipient (no
	// contract creation), carry at least one versioned hash, and every hash
	// must start with the KZG version byte.
	if stx.To == nil {
		return nil, ErrNilToFieldTx
	}
	if len(stx.BlobVersionedHashes) == 0 {
		return nil, ErrBlobTxnEmptyBlobs
	}
	for _, h := range stx.BlobVersionedHashes {
		if h[0] != kzg.BlobCommitmentVersionKZG {
			return nil, ErrBlobTxnInvalidVersionedHash
		}
	}
	stxTo := accounts.InternAddress(*stx.To)
	msg := Message{
		nonce:            stx.Nonce,
		gasLimit:         stx.GasLimit,
		gasPrice:         stx.FeeCap,
		tipCap:           stx.TipCap,
		feeCap:           stx.FeeCap,
		to:               stxTo,
		amount:           stx.Value,
		data:             stx.Data,
		accessList:       stx.AccessList,
		checkNonce:       true,
		checkTransaction: true,
		checkGas:         true,
	}
	if baseFee != nil {
		msg.gasPrice.Set(baseFee)
	}
	msg.gasPrice.Add(&msg.gasPrice, &stx.TipCap)
	if msg.gasPrice.Gt(&stx.FeeCap) {
		msg.gasPrice.Set(&stx.FeeCap)
	}
	var err error
	if msg.from, err = stx.Sender(s); err != nil {
		return nil, err
	}
	msg.maxFeePerBlobGas = stx.MaxFeePerBlobGas
	msg.blobHashes = stx.BlobVersionedHashes
	return &msg, nil
}

func (stx *BlobTx) cachedSender() (sender accounts.Address, ok bool) {
	s := stx.from
	if s.IsNil() {
		return sender, false
	}
	return s, true
}

func (stx *BlobTx) Sender(signer Signer) (accounts.Address, error) {
	if from := stx.from; !from.IsNil() && !from.IsZero() {
		// Sender address can never be zero in a transaction with a valid signer
		return from, nil
	}
	addr, err := signer.Sender(stx)
	if err != nil {
		return accounts.ZeroAddress, err
	}
	stx.from = addr
	return addr, nil
}

func (stx *BlobTx) Hash() common.Hash {
	if hash := stx.hash.Load(); hash != nil {
		return *hash
	}
	payloadSize, accessListLen, blobHashesLen := stx.payloadSize()
	hash := prefixedPayloadHash(BlobTxType, func(w io.Writer, b []byte) error {
		return stx.encodePayload(w, b, payloadSize, accessListLen, blobHashesLen)
	})
	stx.hash.Store(&hash)
	return hash
}

type blobTxSigHash struct {
	ChainID    *uint256.Int
	Nonce      uint64
	GasTipCap  *uint256.Int
	GasFeeCap  *uint256.Int
	Gas        uint64
	To         *common.Address
	Value      *uint256.Int
	Data       []byte
	AccessList AccessList
	BlobFeeCap *uint256.Int
	BlobHashes []common.Hash
}

func (stx *BlobTx) SigningHash(chainID *uint256.Int) common.Hash {
	return prefixedRlpHash(
		BlobTxType,
		&blobTxSigHash{
			ChainID:    chainID,
			Nonce:      stx.Nonce,
			GasTipCap:  &stx.TipCap,
			GasFeeCap:  &stx.FeeCap,
			Gas:        stx.GasLimit,
			To:         stx.To,
			Value:      &stx.Value,
			Data:       stx.Data,
			AccessList: stx.AccessList,
			BlobFeeCap: &stx.MaxFeePerBlobGas,
			BlobHashes: stx.BlobVersionedHashes,
		})
}

func (stx *BlobTx) WithSignature(signer Signer, sig []byte) (Transaction, error) {
	cpy := stx.copy()
	r, s, v, err := signer.SignatureValues(stx, sig)
	if err != nil {
		return nil, err
	}
	cpy.R.Set(r)
	cpy.S.Set(s)
	cpy.V.Set(v)
	cpy.ChainID = *signer.ChainID()
	return cpy, nil
}

func (stx *BlobTx) copy() *BlobTx {
	cpy := &BlobTx{
		DynamicFeeTransaction: *stx.DynamicFeeTransaction.copy(),
		MaxFeePerBlobGas:      stx.MaxFeePerBlobGas,
		BlobVersionedHashes:   make([]common.Hash, len(stx.BlobVersionedHashes)),
	}
	copy(cpy.BlobVersionedHashes, stx.BlobVersionedHashes)
	return cpy
}

func (stx *BlobTx) EncodingSize() int {
	payloadSize, _, _ := stx.payloadSize()
	// Add envelope size and type size
	return 1 + rlp.ListPrefixLen(payloadSize) + payloadSize
}

func (stx *BlobTx) payloadSize() (payloadSize, accessListLen, blobHashesLen int) {
	payloadSize, accessListLen = stx.DynamicFeeTransaction.payloadSize()
	payloadSize += rlp.Uint256Len(stx.MaxFeePerBlobGas)
	// size of BlobVersionedHashes
	blobHashesLen = blobVersionedHashesSize(stx.BlobVersionedHashes)
	payloadSize += rlp.ListPrefixLen(blobHashesLen) + blobHashesLen
	return
}

func blobVersionedHashesSize(hashes []common.Hash) int {
	return 33 * len(hashes)
}

func encodeBlobVersionedHashes(hashes []common.Hash, w io.Writer, b []byte) error {
	for i := range hashes {
		if err := rlp.EncodeString(hashes[i][:], w, b); err != nil {
			return err
		}
	}
	return nil
}

func (stx *BlobTx) encodePayload(w io.Writer, b []byte, payloadSize, accessListLen, blobHashesLen int) error {
	// prefix
	if err := rlp.EncodeListPrefix(payloadSize, w, b); err != nil {
		return err
	}
	// encode ChainID
	if err := rlp.EncodeUint256(stx.ChainID, w, b); err != nil {
		return err
	}
	// encode Nonce
	if err := rlp.EncodeU64(stx.Nonce, w, b); err != nil {
		return err
	}
	// encode MaxPriorityFeePerGas
	if err := rlp.EncodeUint256(stx.TipCap, w, b); err != nil {
		return err
	}
	// encode MaxFeePerGas
	if err := rlp.EncodeUint256(stx.FeeCap, w, b); err != nil {
		return err
	}
	// encode GasLimit
	if err := rlp.EncodeU64(stx.GasLimit, w, b); err != nil {
		return err
	}
	// encode To
	if err := EncodeOptionalAddress(stx.To, w, b); err != nil {
		return err
	}
	// encode Value
	if err := rlp.EncodeUint256(stx.Value, w, b); err != nil {
		return err
	}
	// encode Data
	if err := rlp.EncodeString(stx.Data, w, b); err != nil {
		return err
	}
	// prefix
	if err := rlp.EncodeListPrefix(accessListLen, w, b); err != nil {
		return err
	}
	// encode AccessList
	if err := encodeAccessList(stx.AccessList, w, b); err != nil {
		return err
	}
	// encode MaxFeePerBlobGas
	if err := rlp.EncodeUint256(stx.MaxFeePerBlobGas, w, b); err != nil {
		return err
	}
	// prefix
	if err := rlp.EncodeListPrefix(blobHashesLen, w, b); err != nil {
		return err
	}
	// encode BlobVersionedHashes
	if err := encodeBlobVersionedHashes(stx.BlobVersionedHashes, w, b); err != nil {
		return err
	}
	// encode V
	if err := rlp.EncodeUint256(stx.V, w, b); err != nil {
		return err
	}
	// encode R
	if err := rlp.EncodeUint256(stx.R, w, b); err != nil {
		return err
	}
	// encode S
	if err := rlp.EncodeUint256(stx.S, w, b); err != nil {
		return err
	}
	return nil
}

func (stx *BlobTx) EncodeRLP(w io.Writer) error {
	if stx.To == nil {
		return ErrNilToFieldTx
	}
	payloadSize, accessListLen, blobHashesLen := stx.payloadSize()
	// size of struct prefix and TxType
	envelopeSize := 1 + rlp.ListPrefixLen(payloadSize) + payloadSize
	b := rlp.NewEncodingBuf()
	defer b.Release()
	// envelope
	if err := rlp.EncodeStringPrefix(envelopeSize, w, b[:]); err != nil {
		return err
	}
	// encode TxType
	b[0] = BlobTxType
	if _, err := w.Write(b[:1]); err != nil {
		return err
	}
	if err := stx.encodePayload(w, b[:], payloadSize, accessListLen, blobHashesLen); err != nil {
		return err
	}
	return nil
}

func (stx *BlobTx) MarshalBinary(w io.Writer) error {
	if stx.To == nil {
		return ErrNilToFieldTx
	}
	payloadSize, accessListLen, blobHashesLen := stx.payloadSize()
	b := rlp.NewEncodingBuf()
	defer b.Release()
	// encode TxType
	b[0] = BlobTxType
	if _, err := w.Write(b[:1]); err != nil {
		return err
	}
	if err := stx.encodePayload(w, b[:], payloadSize, accessListLen, blobHashesLen); err != nil {
		return err
	}
	return nil
}

func (stx *BlobTx) DecodeRLP(s *rlp.Stream) error {
	_, err := s.List()
	if err != nil {
		return err
	}
	if err = s.ReadUint256(&stx.ChainID); err != nil {
		return err
	}
	if stx.Nonce, err = s.Uint64(); err != nil {
		return err
	}
	if err = s.ReadUint256(&stx.TipCap); err != nil {
		return err
	}
	if err = s.ReadUint256(&stx.FeeCap); err != nil {
		return err
	}
	if stx.GasLimit, err = s.Uint64(); err != nil {
		return err
	}
	if kind, size, err := s.Kind(); err != nil {
		return err
	} else if kind == rlp.Byte {
		return errors.New("wrong size for To: 1")
	} else if size != length.Addr {
		return fmt.Errorf("wrong size for To: %d", size)
	}
	to, err := s.Addr()
	if err != nil {
		return err
	}
	stx.To = &to
	if err = s.ReadUint256(&stx.Value); err != nil {
		return err
	}
	if stx.Data, err = s.Bytes(); err != nil {
		return err
	}
	// decode AccessList
	stx.AccessList = AccessList{}
	if err = decodeAccessList(&stx.AccessList, s); err != nil {
		return err
	}
	// decode MaxFeePerBlobGas
	if err = s.ReadUint256(&stx.MaxFeePerBlobGas); err != nil {
		return err
	}
	// decode BlobVersionedHashes
	if stx.BlobVersionedHashes, err = decodeHashList(s); err != nil {
		return fmt.Errorf("read BlobVersionedHashes: %w", err)
	}
	if len(stx.BlobVersionedHashes) == 0 {
		return errors.New("a blob stx must contain at least one blob")
	}
	// decode V
	if err = s.ReadUint256(&stx.V); err != nil {
		return err
	}
	if err = s.ReadUint256(&stx.R); err != nil {
		return err
	}
	if err = s.ReadUint256(&stx.S); err != nil {
		return err
	}
	return s.ListEnd()
}
