//! Sealed block types

use crate::{
    block::{error::BlockRecoveryError, RecoveredBlock},
    transaction::signed::RecoveryError,
    Block, BlockBody, GotExpected, InMemorySize, SealedHeader,
};
use alloc::vec::Vec;
use alloy_consensus::BlockHeader as _;
use alloy_eips::{eip1898::BlockWithParent, BlockNumHash};
use alloy_primitives::{Address, BlockHash, Sealable, Sealed, B256};
use alloy_rlp::{Decodable, Encodable};
use bytes::BufMut;
use core::ops::Deref;

/// Sealed full block composed of the block's header and body.
///
/// This type uses lazy sealing to avoid hashing the header until it is needed, see also
/// [`SealedHeader`].
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct SealedBlock<B: Block> {
    /// Sealed Header.
    header: SealedHeader<B::Header>,
    /// the block's body.
    body: B::Body,
}

impl<B: Block> SealedBlock<B> {
    /// Hashes the header and creates a sealed block.
    ///
    /// This calculates the header hash. To create a [`SealedBlock`] without calculating the hash
    /// upfront see [`SealedBlock::new_unhashed`]
    pub fn seal_slow(block: B) -> Self {
        let hash = block.header().hash_slow();
        Self::new_unchecked(block, hash)
    }

    /// Create a new sealed block instance using the block.
    ///
    /// Caution: This assumes the given hash is the block's hash.
    #[inline]
    pub fn new_unchecked(block: B, hash: BlockHash) -> Self {
        let (header, body) = block.split();
        Self { header: SealedHeader::new(header, hash), body }
    }

    /// Creates a `SealedBlock` from the block without the available hash
    #[inline]
    pub fn new_unhashed(block: B) -> Self {
        let (header, body) = block.split();
        Self { header: SealedHeader::new_unhashed(header), body }
    }

    /// Creates the [`SealedBlock`] from the block's parts by hashing the header.
    ///
    ///
    /// This calculates the header hash. To create a [`SealedBlock`] from its parts without
    /// calculating the hash upfront see [`SealedBlock::from_parts_unhashed`]
    pub fn seal_parts(header: B::Header, body: B::Body) -> Self {
        Self::seal_slow(B::new(header, body))
    }

    /// Creates the [`SealedBlock`] from the block's parts without calculating the hash upfront.
    #[inline]
    pub fn from_parts_unhashed(header: B::Header, body: B::Body) -> Self {
        Self::new_unhashed(B::new(header, body))
    }

    /// Creates the [`SealedBlock`] from the block's parts.
    #[inline]
    pub fn from_parts_unchecked(header: B::Header, body: B::Body, hash: BlockHash) -> Self {
        Self::new_unchecked(B::new(header, body), hash)
    }

    /// Creates the [`SealedBlock`] from the [`SealedHeader`] and the body.
    #[inline]
    pub fn from_sealed_parts(header: SealedHeader<B::Header>, body: B::Body) -> Self {
        let (header, hash) = header.split();
        Self::from_parts_unchecked(header, body, hash)
    }

    /// Decodes the block from RLP and seals it.
    pub fn decode_sealed(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
        B::decode_sealed(buf)
    }

    /// Returns a reference to the block hash.
    #[inline]
    pub fn hash_ref(&self) -> &BlockHash {
        self.header.hash_ref()
    }

    /// Returns the block hash.
    #[inline]
    pub fn hash(&self) -> B256 {
        self.header.hash()
    }

    /// Consumes the type and returns its components.
    #[doc(alias = "into_components")]
    #[inline]
    pub fn split(self) -> (B, BlockHash) {
        let (header, hash) = self.header.split();
        (B::new(header, self.body), hash)
    }

    /// Consumes the type and returns the block.
    #[inline]
    pub fn into_block(self) -> B {
        self.unseal()
    }

    /// Consumes the type and returns the block.
    #[inline]
    pub fn unseal(self) -> B {
        let header = self.header.unseal();
        B::new(header, self.body)
    }

    /// Clones the wrapped block.
    pub fn clone_block(&self) -> B {
        B::new(self.header.clone_header(), self.body.clone())
    }

    /// Converts this block into a [`RecoveredBlock`] with the given senders
    ///
    /// Note: This method assumes the senders are correct and does not validate them.
    #[inline]
    pub const fn with_senders(self, senders: Vec<Address>) -> RecoveredBlock<B> {
        RecoveredBlock::new_sealed(self, senders)
    }

    /// Converts this block into a [`RecoveredBlock`] with the given senders if the number of
    /// senders is equal to the number of transactions in the block and recovers the senders from
    /// the transactions, if
    /// not using [`SignedTransaction::recover_signer`](crate::transaction::signed::SignedTransaction)
    /// to recover the senders.
    ///
    /// Returns an error if any of the transactions fail to recover the sender.
    pub fn try_with_senders(
        self,
        senders: Vec<Address>,
    ) -> Result<RecoveredBlock<B>, BlockRecoveryError<Self>> {
        RecoveredBlock::try_recover_sealed_with_senders(self, senders)
    }

    /// Converts this block into a [`RecoveredBlock`] with the given senders if the number of
    /// senders is equal to the number of transactions in the block and recovers the senders from
    /// the transactions, if
    /// not using [`SignedTransaction::recover_signer_unchecked`](crate::transaction::signed::SignedTransaction)
    /// to recover the senders.
    ///
    /// Returns an error if any of the transactions fail to recover the sender.
    pub fn try_with_senders_unchecked(
        self,
        senders: Vec<Address>,
    ) -> Result<RecoveredBlock<B>, BlockRecoveryError<Self>> {
        RecoveredBlock::try_recover_sealed_with_senders_unchecked(self, senders)
    }

    /// Recovers the senders from the transactions in the block using
    /// [`SignedTransaction::recover_signer`](crate::transaction::signed::SignedTransaction).
    ///
    /// Returns an error if any of the transactions fail to recover the sender.
    pub fn try_recover(self) -> Result<RecoveredBlock<B>, BlockRecoveryError<Self>> {
        RecoveredBlock::try_recover_sealed(self)
    }

    /// Recovers the senders from the transactions in the block using
    /// [`SignedTransaction::recover_signer_unchecked`](crate::transaction::signed::SignedTransaction).
    ///
    /// Returns an error if any of the transactions fail to recover the sender.
    pub fn try_recover_unchecked(self) -> Result<RecoveredBlock<B>, BlockRecoveryError<Self>> {
        RecoveredBlock::try_recover_sealed_unchecked(self)
    }

    /// Returns reference to block header.
    #[inline]
    pub const fn header(&self) -> &B::Header {
        self.header.header()
    }

    /// Returns reference to block body.
    #[inline]
    pub const fn body(&self) -> &B::Body {
        &self.body
    }

    /// Returns the length of the block.
    pub fn rlp_length(&self) -> usize {
        B::rlp_length(self.header(), self.body())
    }

    /// Recovers all senders from the transactions in the block.
    ///
    /// Returns an error if any of the transactions fail to recover the sender.
    pub fn senders(&self) -> Result<Vec<Address>, RecoveryError> {
        self.body().recover_signers()
    }

    /// Return the number hash tuple.
    #[inline]
    pub fn num_hash(&self) -> BlockNumHash {
        BlockNumHash::new(self.number(), self.hash())
    }

    /// Return a [`BlockWithParent`] for this header.
    #[inline]
    pub fn block_with_parent(&self) -> BlockWithParent {
        BlockWithParent { parent: self.parent_hash(), block: self.num_hash() }
    }

    /// Returns the Sealed header.
    #[inline]
    pub const fn sealed_header(&self) -> &SealedHeader<B::Header> {
        &self.header
    }

    /// Returns the wrapped `SealedHeader<B::Header>` as `SealedHeader<&B::Header>`.
    #[inline]
    pub fn sealed_header_ref(&self) -> SealedHeader<&B::Header> {
        SealedHeader::new(self.header(), self.hash())
    }

    /// Clones the wrapped header and returns a [`SealedHeader`] sealed with the hash.
    pub fn clone_sealed_header(&self) -> SealedHeader<B::Header> {
        self.header.clone()
    }

    /// Consumes the block and returns the sealed header.
    #[inline]
    pub fn into_sealed_header(self) -> SealedHeader<B::Header> {
        self.header
    }

    /// Consumes the block and returns the header.
    #[inline]
    pub fn into_header(self) -> B::Header {
        self.header.unseal()
    }

    /// Consumes the block and returns the body.
    #[inline]
    pub fn into_body(self) -> B::Body {
        self.body
    }

    /// Splits the block into body and header into separate components
    #[inline]
    pub fn split_header_body(self) -> (B::Header, B::Body) {
        let header = self.header.unseal();
        (header, self.body)
    }

    /// Splits the block into body and header into separate components.
    pub fn split_sealed_header_body(self) -> (SealedHeader<B::Header>, B::Body) {
        (self.header, self.body)
    }

    /// Returns an iterator over all blob versioned hashes from the block body.
    #[inline]
    pub fn blob_versioned_hashes_iter(&self) -> impl Iterator<Item = &B256> + '_ {
        self.body().blob_versioned_hashes_iter()
    }

    /// Returns the number of transactions in the block.
    #[inline]
    pub fn transaction_count(&self) -> usize {
        self.body().transaction_count()
    }

    /// Ensures that the transaction root in the block header is valid.
    ///
    /// The transaction root is the Keccak 256-bit hash of the root node of the trie structure
    /// populated with each transaction in the transactions list portion of the block.
    ///
    /// # Returns
    ///
    /// Returns `Ok(())` if the calculated transaction root matches the one stored in the header,
    /// indicating that the transactions in the block are correctly represented in the trie.
    ///
    /// Returns `Err(error)` if the transaction root validation fails, providing a `GotExpected`
    /// error containing the calculated and expected roots.
    pub fn ensure_transaction_root_valid(&self) -> Result<(), GotExpected<B256>> {
        let calculated_root = self.body().calculate_tx_root();

        if self.header().transactions_root() != calculated_root {
            return Err(GotExpected {
                got: calculated_root,
                expected: self.header().transactions_root(),
            })
        }

        Ok(())
    }
}

impl<B> From<B> for SealedBlock<B>
where
    B: Block,
{
    #[inline]
    fn from(block: B) -> Self {
        Self::seal_slow(block)
    }
}

impl<B> Default for SealedBlock<B>
where
    B: Block + Default,
{
    #[inline]
    fn default() -> Self {
        Self::seal_slow(Default::default())
    }
}

impl<B: Block> InMemorySize for SealedBlock<B> {
    #[inline]
    fn size(&self) -> usize {
        self.body.size() + self.header.size()
    }
}

impl<B: Block> Deref for SealedBlock<B> {
    type Target = B::Header;

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.header()
    }
}

impl<B: Block> Encodable for SealedBlock<B> {
    fn encode(&self, out: &mut dyn BufMut) {
        B::rlp_encode(self.header(), self.body(), out);
    }

    fn length(&self) -> usize {
        self.rlp_length()
    }
}

impl<B: Block> Decodable for SealedBlock<B> {
    fn decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
        B::decode_sealed(buf)
    }
}

impl<B: Block> From<SealedBlock<B>> for Sealed<B> {
    #[inline]
    fn from(value: SealedBlock<B>) -> Self {
        let (block, hash) = value.split();
        Self::new_unchecked(block, hash)
    }
}

impl<B: Block> From<Sealed<B>> for SealedBlock<B> {
    #[inline]
    fn from(value: Sealed<B>) -> Self {
        let (block, hash) = value.into_parts();
        Self::new_unchecked(block, hash)
    }
}

#[cfg(any(test, feature = "arbitrary"))]
impl<'a, B> arbitrary::Arbitrary<'a> for SealedBlock<B>
where
    B: Block + arbitrary::Arbitrary<'a>,
{
    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
        let block = B::arbitrary(u)?;
        Ok(Self::seal_slow(block))
    }
}

#[cfg(any(test, feature = "test-utils"))]
impl<B: crate::test_utils::TestBlock> SealedBlock<B> {
    /// Returns a mutable reference to the header.
    #[inline]
    pub const fn header_mut(&mut self) -> &mut B::Header {
        self.header.header_mut()
    }

    /// Updates the block hash.
    #[inline]
    pub fn set_hash(&mut self, hash: BlockHash) {
        self.header.set_hash(hash)
    }

    /// Returns a mutable reference to the body.
    #[inline]
    pub const fn body_mut(&mut self) -> &mut B::Body {
        &mut self.body
    }

    /// Updates the parent block hash.
    #[inline]
    pub fn set_parent_hash(&mut self, hash: BlockHash) {
        self.header.set_parent_hash(hash)
    }

    /// Updates the block number.
    #[inline]
    pub fn set_block_number(&mut self, number: alloy_primitives::BlockNumber) {
        self.header.set_block_number(number)
    }

    /// Updates the block timestamp.
    #[inline]
    pub fn set_timestamp(&mut self, timestamp: u64) {
        self.header.set_timestamp(timestamp)
    }

    /// Updates the block state root.
    #[inline]
    pub fn set_state_root(&mut self, state_root: alloy_primitives::B256) {
        self.header.set_state_root(state_root)
    }

    /// Updates the block difficulty.
    #[inline]
    pub fn set_difficulty(&mut self, difficulty: alloy_primitives::U256) {
        self.header.set_difficulty(difficulty)
    }
}

/// A [`SealedBlock`] paired with associated data.
///
/// This is useful for workflows that need to carry block-adjacent metadata alongside a sealed
/// block without defining a new wrapper type for each metadata payload.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(
    feature = "serde",
    serde(bound(
        serialize = "SealedBlock<B>: serde::Serialize, T: serde::Serialize",
        deserialize = "SealedBlock<B>: serde::Deserialize<'de>, T: serde::Deserialize<'de>"
    ))
)]
pub struct SealedBlockWith<B: Block, T> {
    /// The sealed block.
    block: SealedBlock<B>,
    /// Associated data for the sealed block.
    data: T,
}

impl<B: Block, T> SealedBlockWith<B, T> {
    /// Creates a new sealed block with associated data.
    #[inline]
    pub const fn new(block: SealedBlock<B>, data: T) -> Self {
        Self { block, data }
    }

    /// Returns the sealed block.
    #[inline]
    pub const fn block(&self) -> &SealedBlock<B> {
        &self.block
    }

    /// Returns the associated data.
    #[inline]
    pub const fn data(&self) -> &T {
        &self.data
    }

    /// Consumes the type and returns its components.
    #[doc(alias = "into_parts")]
    #[inline]
    pub fn split(self) -> (SealedBlock<B>, T) {
        (self.block, self.data)
    }
}

impl<B: Block, T> SealedBlockWith<B, Option<T>> {
    /// Creates a sealed block without associated data.
    #[inline]
    pub const fn from_block(block: SealedBlock<B>) -> Self {
        Self::new(block, None)
    }
}

impl<B: Block, T> From<(SealedBlock<B>, T)> for SealedBlockWith<B, T> {
    #[inline]
    fn from((block, data): (SealedBlock<B>, T)) -> Self {
        Self::new(block, data)
    }
}

impl<B: Block, T> From<SealedBlock<B>> for SealedBlockWith<B, Option<T>> {
    #[inline]
    fn from(block: SealedBlock<B>) -> Self {
        Self::from_block(block)
    }
}

impl<B: Block, T: InMemorySize> InMemorySize for SealedBlockWith<B, T> {
    #[inline]
    fn size(&self) -> usize {
        self.block.size() + self.data.size()
    }
}

impl<B: Block, T> Deref for SealedBlockWith<B, T> {
    type Target = SealedBlock<B>;

    #[inline]
    fn deref(&self) -> &Self::Target {
        self.block()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_rlp::{Decodable, Encodable};

    fn sample_alloy_block() -> alloy_consensus::Block<alloy_consensus::TxEnvelope> {
        let header = alloy_consensus::Header {
            number: 42,
            gas_limit: 30_000_000,
            gas_used: 21_000,
            timestamp: 1_000_000,
            base_fee_per_gas: Some(1_000_000_000),
            ..Default::default()
        };

        let tx = alloy_consensus::TxLegacy {
            chain_id: Some(1),
            nonce: 0,
            gas_price: 21_000_000_000,
            gas_limit: 21_000,
            to: alloy_primitives::TxKind::Call(Address::ZERO),
            value: alloy_primitives::U256::from(100),
            input: alloy_primitives::Bytes::default(),
        };

        let tx_signed =
            alloy_consensus::TxEnvelope::Legacy(alloy_consensus::Signed::new_unchecked(
                tx,
                alloy_primitives::Signature::test_signature(),
                B256::ZERO,
            ));

        let body = alloy_consensus::BlockBody {
            transactions: vec![tx_signed],
            ommers: vec![],
            withdrawals: Some(Default::default()),
        };

        alloy_consensus::Block::new(header, body)
    }

    #[test]
    fn test_sealed_block_rlp_roundtrip() {
        // Create a sample block using alloy_consensus::Block
        let header = alloy_consensus::Header {
            number: 42,
            gas_limit: 30_000_000,
            gas_used: 21_000,
            timestamp: 1_000_000,
            base_fee_per_gas: Some(1_000_000_000),
            ..Default::default()
        };

        // Create a simple transaction
        let tx = alloy_consensus::TxLegacy {
            chain_id: Some(1),
            nonce: 0,
            gas_price: 21_000_000_000,
            gas_limit: 21_000,
            to: alloy_primitives::TxKind::Call(Address::ZERO),
            value: alloy_primitives::U256::from(100),
            input: alloy_primitives::Bytes::default(),
        };

        let tx_signed =
            alloy_consensus::TxEnvelope::Legacy(alloy_consensus::Signed::new_unchecked(
                tx,
                alloy_primitives::Signature::test_signature(),
                B256::ZERO,
            ));

        // Create block body with the transaction
        let body = alloy_consensus::BlockBody {
            transactions: vec![tx_signed],
            ommers: vec![],
            withdrawals: Some(Default::default()),
        };

        // Create the block
        let block = alloy_consensus::Block::new(header, body);

        // Create a sealed block
        let sealed_block = SealedBlock::seal_slow(block);

        // Encode the sealed block
        let mut encoded = Vec::new();
        sealed_block.encode(&mut encoded);

        // Decode the sealed block
        let decoded = SealedBlock::<
            alloy_consensus::Block<alloy_consensus::TxEnvelope, alloy_consensus::Header>,
        >::decode(&mut encoded.as_slice())
        .expect("Failed to decode sealed block");

        // Verify the roundtrip
        assert_eq!(sealed_block.hash(), decoded.hash());
        assert_eq!(sealed_block.header().number, decoded.header().number);
        assert_eq!(sealed_block.header().state_root, decoded.header().state_root);
        assert_eq!(sealed_block.body().transactions.len(), decoded.body().transactions.len());
    }

    #[test]
    fn test_alloy_block_sealed_encoding_matches_regular_block() {
        let block = sample_alloy_block();

        let mut block_encoded = Vec::new();
        block.encode(&mut block_encoded);

        let mut borrowed_encoded = Vec::new();
        <alloy_consensus::Block<alloy_consensus::TxEnvelope> as Block>::rlp_encode(
            &block.header,
            &block.body,
            &mut borrowed_encoded,
        );

        let sealed_block = SealedBlock::seal_slow(block.clone());
        let mut sealed_encoded = Vec::new();
        sealed_block.encode(&mut sealed_encoded);

        assert_eq!(borrowed_encoded, block_encoded);
        assert_eq!(sealed_encoded, block_encoded);
        assert_eq!(sealed_block.length(), block.length());
    }

    #[test]
    fn test_decode_sealed_produces_correct_hash() {
        // Create a sample block using alloy_consensus::Block
        let header = alloy_consensus::Header {
            number: 42,
            gas_limit: 30_000_000,
            gas_used: 21_000,
            timestamp: 1_000_000,
            base_fee_per_gas: Some(1_000_000_000),
            ..Default::default()
        };

        // Create a simple transaction
        let tx = alloy_consensus::TxLegacy {
            chain_id: Some(1),
            nonce: 0,
            gas_price: 21_000_000_000,
            gas_limit: 21_000,
            to: alloy_primitives::TxKind::Call(Address::ZERO),
            value: alloy_primitives::U256::from(100),
            input: alloy_primitives::Bytes::default(),
        };

        let tx_signed =
            alloy_consensus::TxEnvelope::Legacy(alloy_consensus::Signed::new_unchecked(
                tx,
                alloy_primitives::Signature::test_signature(),
                B256::ZERO,
            ));

        // Create block body with the transaction
        let body = alloy_consensus::BlockBody {
            transactions: vec![tx_signed],
            ommers: vec![],
            withdrawals: Some(Default::default()),
        };

        // Create the block
        let block = alloy_consensus::Block::new(header, body);
        let expected_hash = block.header.hash_slow();

        // Encode the block
        let mut encoded = Vec::new();
        block.encode(&mut encoded);

        // Decode using decode_sealed - this should compute hash from raw RLP
        let decoded =
            SealedBlock::<alloy_consensus::Block<alloy_consensus::TxEnvelope>>::decode_sealed(
                &mut encoded.as_slice(),
            )
            .expect("Failed to decode sealed block");

        // Verify the hash matches
        assert_eq!(decoded.hash(), expected_hash);
        assert_eq!(decoded.header().number, 42);
        assert_eq!(decoded.body().transactions.len(), 1);
    }

    #[test]
    fn test_sealed_block_from_sealed() {
        let header = alloy_consensus::Header::default();
        let body = alloy_consensus::BlockBody::<alloy_consensus::TxEnvelope>::default();
        let block = alloy_consensus::Block::new(header, body);
        let hash = block.header.hash_slow();

        // Create Sealed<Block>
        let sealed: Sealed<alloy_consensus::Block<alloy_consensus::TxEnvelope>> =
            Sealed::new_unchecked(block.clone(), hash);

        // Convert to SealedBlock
        let sealed_block: SealedBlock<alloy_consensus::Block<alloy_consensus::TxEnvelope>> =
            SealedBlock::from(sealed);

        assert_eq!(sealed_block.hash(), hash);
        assert_eq!(sealed_block.header().number, block.header.number);
    }

    #[test]
    fn test_sealed_block_with_data() {
        let block = alloy_consensus::Block::<alloy_consensus::TxEnvelope>::default();
        let sealed_block = SealedBlock::seal_slow(block);

        let with_data = SealedBlockWith::new(sealed_block.clone(), Some(42u64));

        assert_eq!(&*with_data, &sealed_block);
        assert_eq!(with_data.hash(), sealed_block.hash());
        assert_eq!(with_data.block(), &sealed_block);
        assert_eq!(with_data.data(), &Some(42));

        let (block, data) = with_data.split();
        assert_eq!(block, sealed_block);
        assert_eq!(data, Some(42));
    }

    #[test]
    fn test_sealed_block_with_from_block() {
        let block = alloy_consensus::Block::<alloy_consensus::TxEnvelope>::default();
        let sealed_block = SealedBlock::seal_slow(block);

        let with_data = SealedBlockWith::<_, Option<u64>>::from_block(sealed_block.clone());
        assert_eq!(with_data.block(), &sealed_block);
        assert_eq!(with_data.data(), &None);

        let from_block: SealedBlockWith<_, Option<u64>> = sealed_block.into();
        assert_eq!(from_block.data(), &None);
    }
}
