//! reth `NodePrimitives` integration for Arbitrum types.
//!
//! Gated behind the `reth` feature. Binds [`ArbPrimitives`] to reth's
//! [`NodePrimitives`](reth_primitives_traits::NodePrimitives) trait so the
//! arb-reth-evm executor can use Arbitrum block/tx/receipt types.

use alloy_consensus::InMemorySize;
use alloy_primitives::Log;

use crate::{ArbReceiptEnvelope, ArbTxEnvelope};

// ---------------------------------------------------------------------------
// DB codec: Compact + Compress + Decompress for ArbTxEnvelope and ArbReceiptEnvelope<Log>
//
// reth's `Value` trait (reth_db_api::table::Value) requires Compress + Decompress + Serialize.
// `reth_primitives_traits::MaybeCompact` (when the `reth-codec` feature is enabled) additionally
// requires `reth_codecs::Compact`.
//
// We use EIP-2718 encoding as the on-disk format: the compact buffer IS the 2718 bytes.
// The `len` parameter to `from_compact` is the full buffer length in our case (we write all
// remaining bytes and consume all of them on decode).
//
// `Serialize` for `ArbTxEnvelope` is generated by the `TransactionEnvelope` derive macro when
// the `alloy-consensus/serde` feature is active, which is always the case when this crate is
// built with the `reth` feature (see Cargo.toml).
// ---------------------------------------------------------------------------

impl reth_codecs::Compact for ArbTxEnvelope {
    fn to_compact<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) -> usize {
        use alloy_eips::Encodable2718;
        let len = self.encode_2718_len();
        self.encode_2718(buf);
        len
    }

    fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
        use alloy_eips::Decodable2718;
        let mut rest = buf;
        let tx = Self::decode_2718(&mut rest)
            .expect("ArbTxEnvelope Compact::from_compact: invalid 2718 encoding");
        (tx, rest)
    }
}

// `impl_compression_for_compact!` generates Compress + Decompress via Compact.
reth_codecs::impl_compression_for_compact!(ArbTxEnvelope);

// The on-disk receipt format is:  [gas_used_for_l1: u64 BE, 8 bytes] ++ [EIP-2718 receipt bytes].
//
// CAUTION: the EIP-2718 receipt encoding is the *consensus* form and does NOT carry Arbitrum's
// `gas_used_for_l1` field (that lives only at the implementation/RPC layer). A naive 2718-only
// codec is therefore LOSSY, it silently drops `gas_used_for_l1`, which would corrupt receipt
// storage. We prepend the field as a fixed 8-byte big-endian prefix so the roundtrip is exact.
// The 2718 decode is self-delimiting (RLP length header), so it consumes exactly the receipt
// bytes after the fixed prefix.
const GAS_USED_FOR_L1_PREFIX_LEN: usize = 8;

impl reth_codecs::Compact for ArbReceiptEnvelope<Log> {
    fn to_compact<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) -> usize {
        use alloy_eips::Encodable2718;
        let gas_used_for_l1 = self.as_receipt_with_bloom().receipt.gas_used_for_l1;
        buf.put_slice(&gas_used_for_l1.to_be_bytes());
        let body_len = self.encode_2718_len();
        self.encode_2718(buf);
        GAS_USED_FOR_L1_PREFIX_LEN + body_len
    }

    fn from_compact(buf: &[u8], _len: usize) -> (Self, &[u8]) {
        use alloy_eips::Decodable2718;
        assert!(
            buf.len() >= GAS_USED_FOR_L1_PREFIX_LEN,
            "ArbReceiptEnvelope Compact::from_compact: buffer too short for gas_used_for_l1 prefix"
        );
        let (prefix, mut rest) = buf.split_at(GAS_USED_FOR_L1_PREFIX_LEN);
        let gas_used_for_l1 = u64::from_be_bytes(
            prefix.try_into().expect("prefix is exactly 8 bytes"),
        );
        let mut receipt = Self::decode_2718(&mut rest)
            .expect("ArbReceiptEnvelope Compact::from_compact: invalid 2718 encoding");
        // Re-attach the Arbitrum-specific field dropped by the consensus 2718 form.
        receipt.as_receipt_with_bloom_mut().receipt.gas_used_for_l1 = gas_used_for_l1;
        (receipt, rest)
    }
}

// Manual Compress + Decompress for the concrete ArbReceiptEnvelope<Log> type.
// (Cannot use impl_compression_for_compact! here because the macro adds a generic bound
// `Log: Compact` which would create a broader generic impl overlapping with the concrete one.)
impl reth_codecs::Compress for ArbReceiptEnvelope<Log> {
    type Compressed = alloc::vec::Vec<u8>;

    fn compress_to_buf<B: bytes::BufMut + AsMut<[u8]>>(&self, buf: &mut B) {
        let _ = reth_codecs::Compact::to_compact(self, buf);
    }
}

impl reth_codecs::Decompress for ArbReceiptEnvelope<Log> {
    fn decompress(value: &[u8]) -> Result<Self, reth_codecs::DecompressError> {
        let (obj, _) = reth_codecs::Compact::from_compact(value, value.len());
        Ok(obj)
    }
}

/// Arbitrum block type (alloy-consensus Block with Arbitrum txs; Header via extra_data/mix_hash).
pub type ArbBlock = alloy_consensus::Block<ArbTxEnvelope>;

/// Arbitrum block body type.
pub type ArbBlockBody = alloy_consensus::BlockBody<ArbTxEnvelope>;

/// Arbitrum node primitives, binds Arbitrum tx/receipt/block types to reth's trait system.
///
/// Mirrors `EthPrimitives` from `reth-ethereum-primitives`.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ArbPrimitives;

impl reth_primitives_traits::NodePrimitives for ArbPrimitives {
    type Block = ArbBlock;
    type BlockHeader = alloy_consensus::Header;
    type BlockBody = ArbBlockBody;
    type SignedTx = ArbTxEnvelope;
    type Receipt = ArbReceiptEnvelope<Log>;
}

// ---------------------------------------------------------------------------
// InMemorySize for ArbTxEnvelope
// ---------------------------------------------------------------------------

impl InMemorySize for ArbTxEnvelope {
    fn size(&self) -> usize {
        use alloy_eips::Encodable2718;
        // For Ethereum variants, Signed<T>::size() is already impl'd in alloy-consensus.
        match self {
            Self::Legacy(tx) => tx.size(),
            Self::Eip2930(tx) => tx.size(),
            Self::Eip1559(tx) => tx.size(),
            Self::Eip7702(tx) => tx.size(),
            // Arbitrum-specific sealed variants: 2718 encoded length as heuristic.
            Self::Deposit(tx) => tx.encode_2718_len(),
            Self::SubmitRetryable(tx) => tx.encode_2718_len(),
            Self::Unsigned(tx) => tx.encode_2718_len(),
            Self::Contract(tx) => tx.encode_2718_len(),
            Self::Retry(tx) => tx.encode_2718_len(),
            Self::Internal(tx) => tx.encode_2718_len(),
        }
    }
}

// ---------------------------------------------------------------------------
// InMemorySize for ArbReceiptEnvelope<Log>
//
// Only impl for T: alloy_rlp::Encodable + ... so that Encodable2718 is satisfied
// (the existing impl on ArbReceiptEnvelope<T> requires T: Encodable + Send + Sync).
// ---------------------------------------------------------------------------

impl InMemorySize for ArbReceiptEnvelope<Log> {
    fn size(&self) -> usize {
        use alloy_consensus::TxReceipt;
        // Report the true in-memory footprint (used by reth for cache
        // sizing), not the 2718-encoded length. That is the envelope struct itself plus the heap
        // owned by the logs vector (each `Log` carries its own heap-allocated topics + data).
        core::mem::size_of::<Self>()
            + self.logs().iter().map(InMemorySize::size).sum::<usize>()
    }
}

// ---------------------------------------------------------------------------
// RlpEncodableReceipt + RlpDecodableReceipt on ArbReceiptEnvelope<Log>
// (envelope-level, required by reth Receipt supertrait)
// ---------------------------------------------------------------------------

impl alloy_consensus::RlpEncodableReceipt for ArbReceiptEnvelope<Log> {
    fn rlp_encoded_length_with_bloom(&self, bloom: &alloy_primitives::Bloom) -> usize {
        self.as_receipt_with_bloom().receipt.rlp_encoded_length_with_bloom(bloom)
    }

    fn rlp_encode_with_bloom(
        &self,
        bloom: &alloy_primitives::Bloom,
        out: &mut dyn alloy_rlp::BufMut,
    ) {
        self.as_receipt_with_bloom().receipt.rlp_encode_with_bloom(bloom, out)
    }
}

impl alloy_consensus::RlpDecodableReceipt for ArbReceiptEnvelope<Log> {
    fn rlp_decode_with_bloom(
        buf: &mut &[u8],
    ) -> alloy_rlp::Result<alloy_consensus::ReceiptWithBloom<Self>> {
        use alloy_consensus::RlpDecodableReceipt;
        // This is the *untyped* (bare-RLP) decode entry point, the legacy fallback used by
        // `Decodable2718::fallback_decode`. A bare RLP receipt carries no 2718 type byte, so the
        // only correct variant here is `Legacy` (typed receipts are recovered by `typed_decode`,
        // which is already correct in `receipt.rs`).
        //
        // The inner `ArbReceipt<Log>` decode already produces the real
        // `logs_bloom`; carry it through to the *outer* `ReceiptWithBloom` instead of zeroing it,
        // so a round-trip (`rlp_encode_with_bloom` -> `rlp_decode_with_bloom`) and any
        // bloom-dependent consumer sees the correct bloom.
        let alloy_consensus::ReceiptWithBloom { receipt, logs_bloom } =
            <crate::ArbReceipt<Log> as RlpDecodableReceipt>::rlp_decode_with_bloom(buf)?;
        Ok(alloy_consensus::ReceiptWithBloom {
            receipt: ArbReceiptEnvelope::Legacy(alloy_consensus::ReceiptWithBloom {
                receipt,
                logs_bloom,
            }),
            logs_bloom,
        })
    }
}

// ---------------------------------------------------------------------------
// Eip2718EncodableReceipt on ArbReceiptEnvelope<Log>
// ---------------------------------------------------------------------------

impl alloy_consensus::Eip2718EncodableReceipt for ArbReceiptEnvelope<Log> {
    fn eip2718_encoded_length_with_bloom(&self, bloom: &alloy_primitives::Bloom) -> usize {
        use alloy_consensus::RlpEncodableReceipt;
        use alloy_eips::Typed2718;
        let rlp_len = self.rlp_encoded_length_with_bloom(bloom);
        if self.is_legacy() {
            rlp_len
        } else {
            1 + rlp_len
        }
    }

    fn eip2718_encode_with_bloom(
        &self,
        bloom: &alloy_primitives::Bloom,
        out: &mut dyn alloy_rlp::BufMut,
    ) {
        use alloy_consensus::RlpEncodableReceipt;
        use alloy_eips::Typed2718;
        if !self.is_legacy() {
            out.put_u8(self.ty());
        }
        self.rlp_encode_with_bloom(bloom, out);
    }
}

// ---------------------------------------------------------------------------
// Static assertion test
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{
        ArbReceipt,
        transactions::{
            TxContract, TxDeposit, TxRetry, TxUnsigned, internal::ArbInternalTx,
            submit_retryable::SubmitRetryableTx,
        },
    };
    use alloc::vec;
    use alloy_consensus::{
        Eip658Value, Receipt, ReceiptWithBloom, Signed, TxEip1559, TxEip2930, TxEip7702, TxLegacy,
    };
    use alloy_primitives::{Bytes, Signature, TxKind, U256, address};
    use reth_codecs::{Compact, Compress, Decompress};

    fn _assert_node_primitives<T: reth_primitives_traits::NodePrimitives>() {}
    fn _assert_signed_tx<T: reth_primitives_traits::SignedTransaction>() {}

    #[test]
    fn arb_primitives_satisfies_reth() {
        _assert_node_primitives::<ArbPrimitives>();
        _assert_signed_tx::<ArbTxEnvelope>();
    }

    // -----------------------------------------------------------------------
    // DB-codec roundtrip tests.
    //
    // Each representative value must survive:
    //   * Compress -> Decompress  (the on-disk table path used by reth's `Value`)
    //   * Compact::to_compact -> Compact::from_compact  (the underlying mechanism)
    // and must report/consume exactly the right byte count.
    // -----------------------------------------------------------------------

    /// Asserts a value is lossless through Compress/Decompress AND Compact, and that the
    /// Compact byte accounting is exact (to_compact returns N, from_compact consumes all N).
    fn assert_tx_roundtrip(label: &str, tx: ArbTxEnvelope) {
        // Compress / Decompress path (what reth actually calls for a table `Value`).
        let compressed = tx.clone().compress();
        let decompressed = ArbTxEnvelope::decompress(&compressed)
            .unwrap_or_else(|e| panic!("{label}: decompress failed: {e:?}"));
        assert_eq!(decompressed, tx, "{label}: Compress/Decompress not lossless");

        // Compact path with explicit byte accounting.
        let mut buf = alloc::vec::Vec::new();
        let written = tx.to_compact(&mut buf);
        assert_eq!(
            written,
            buf.len(),
            "{label}: to_compact return value must equal bytes written"
        );
        let (decoded, rest) = ArbTxEnvelope::from_compact(&buf, buf.len());
        assert_eq!(decoded, tx, "{label}: Compact not lossless");
        assert!(
            rest.is_empty(),
            "{label}: from_compact left {} trailing byte(s)",
            rest.len()
        );
    }

    #[test]
    fn arb_tx_envelope_codec_roundtrip_all_variants() {
        let chain_id = U256::from(42161u64);
        let cid: u64 = 42161;
        let addr1 = address!("0x1111111111111111111111111111111111111111");
        let addr2 = address!("0x2222222222222222222222222222222222222222");
        let addr3 = address!("0x3333333333333333333333333333333333333333");
        let addr4 = address!("0x4444444444444444444444444444444444444444");
        let addr5 = address!("0x5555555555555555555555555555555555555555");
        let req = alloy_primitives::B256::from([0xaa; 32]);
        let sig = Signature::test_signature();

        // --- Ethereum variants (signed) ---
        let legacy = TxLegacy {
            chain_id: Some(cid),
            nonce: 1,
            gas_price: 1_000_000_000,
            gas_limit: 21_000,
            to: TxKind::Call(addr2),
            value: U256::from(7u64),
            input: Bytes::from(vec![0x01, 0x02, 0x03]),
        };
        assert_tx_roundtrip(
            "Legacy",
            ArbTxEnvelope::Legacy(Signed::new_unhashed(legacy, sig)),
        );

        let eip2930 = TxEip2930 {
            chain_id: cid,
            nonce: 2,
            gas_price: 1_000_000_000,
            gas_limit: 30_000,
            to: TxKind::Call(addr2),
            value: U256::from(8u64),
            access_list: Default::default(),
            input: Bytes::from(vec![0xaa, 0xbb]),
        };
        assert_tx_roundtrip(
            "Eip2930",
            ArbTxEnvelope::Eip2930(Signed::new_unhashed(eip2930, sig)),
        );

        let eip1559 = TxEip1559 {
            chain_id: cid,
            nonce: 3,
            gas_limit: 40_000,
            max_fee_per_gas: 2_000_000_000,
            max_priority_fee_per_gas: 1_000_000_000,
            to: TxKind::Call(addr2),
            value: U256::from(9u64),
            access_list: Default::default(),
            input: Bytes::from(vec![0xde, 0xad]),
        };
        assert_tx_roundtrip(
            "Eip1559",
            ArbTxEnvelope::Eip1559(Signed::new_unhashed(eip1559, sig)),
        );

        let eip7702 = TxEip7702 {
            chain_id: cid,
            nonce: 4,
            gas_limit: 50_000,
            max_fee_per_gas: 2_000_000_000,
            max_priority_fee_per_gas: 1_000_000_000,
            to: addr2,
            value: U256::from(10u64),
            access_list: Default::default(),
            authorization_list: vec![],
            input: Bytes::from(vec![0xbe, 0xef]),
        };
        assert_tx_roundtrip(
            "Eip7702",
            ArbTxEnvelope::Eip7702(Signed::new_unhashed(eip7702, sig)),
        );

        // --- Arbitrum-specific variants ---
        let deposit = TxDeposit {
            chain_id,
            request_id: req,
            from: addr1,
            to: addr2,
            value: U256::from(123u64),
        };
        assert_tx_roundtrip("Deposit", ArbTxEnvelope::from(deposit));

        let submit = SubmitRetryableTx::new(
            chain_id,
            req,
            addr1,
            U256::from(100u64),
            U256::from(200u64),
            U256::from(300u64),
            U256::from(400000u64),
            TxKind::Call(addr2),
            U256::from(50u64),
            addr4,
            U256::from(600u64),
            addr5,
            Bytes::from(vec![0x01, 0x02, 0x03, 0x04]),
        );
        assert_tx_roundtrip("SubmitRetryable", ArbTxEnvelope::from(submit));

        let unsigned = TxUnsigned {
            chain_id,
            from: addr1,
            nonce: 7,
            gas_fee_cap: U256::from(12345u64),
            gas_limit: 21000,
            to: TxKind::Call(addr2),
            value: U256::from(100u64),
            input: Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]),
        };
        assert_tx_roundtrip("Unsigned", ArbTxEnvelope::from(unsigned));

        let contract = TxContract {
            chain_id,
            request_id: req,
            from: addr1,
            gas_fee_cap: U256::from(12345u64),
            gas_limit: 50000,
            to: TxKind::Call(addr2),
            value: U256::from(1u64),
            input: Bytes::from(vec![0xab, 0xcd, 0xef]),
        };
        assert_tx_roundtrip("Contract", ArbTxEnvelope::from(contract));

        let retry = TxRetry {
            chain_id,
            nonce: 3,
            from: addr1,
            gas_fee_cap: U256::from(555u64),
            gas_limit: 80000,
            to: TxKind::Call(addr2),
            value: U256::from(5u64),
            input: Bytes::from(vec![0x01, 0x02]),
            ticket_id: req,
            refund_to: addr3,
            max_refund: U256::from(1000u64),
            submission_fee_refund: U256::from(2000u64),
        };
        assert_tx_roundtrip("Retry", ArbTxEnvelope::from(retry));

        let internal =
            ArbInternalTx::new(cid, Bytes::from(vec![0x11, 0x22, 0x33, 0x44, 0x55]));
        assert_tx_roundtrip("Internal", ArbTxEnvelope::from(internal));
    }

    #[test]
    fn arb_receipt_envelope_codec_roundtrip() {
        use alloy_consensus::TxReceipt;

        let log1 = alloy_primitives::Log::new(
            address!("0x1111111111111111111111111111111111111111"),
            vec![alloy_primitives::B256::from([0x01; 32])],
            Bytes::from(vec![0xde, 0xad, 0xbe, 0xef]),
        )
        .expect("valid log");
        let log2 = alloy_primitives::Log::new(
            address!("0x2222222222222222222222222222222222222222"),
            vec![
                alloy_primitives::B256::from([0x02; 32]),
                alloy_primitives::B256::from([0x03; 32]),
            ],
            Bytes::from(vec![0xca, 0xfe]),
        )
        .expect("valid log");

        let inner = Receipt {
            status: Eip658Value::Eip658(true),
            cumulative_gas_used: 123_456,
            logs: vec![log1, log2],
        };
        // Compute the real bloom so the 2718 encoding is canonical (bloom is part of the wire form).
        let logs_bloom = inner.bloom_slow();
        let arb = ArbReceipt {
            inner,
            gas_used_for_l1: 4_242,
        };
        // Use the Eip1559 (type 0x2) variant as a representative typed receipt.
        let receipt = ArbReceiptEnvelope::Eip1559(ReceiptWithBloom::new(arb, logs_bloom));

        // Compress / Decompress path.
        let compressed = receipt.clone().compress();
        let decompressed =
            ArbReceiptEnvelope::<Log>::decompress(&compressed).expect("decompress");
        assert_eq!(
            decompressed, receipt,
            "ArbReceiptEnvelope Compress/Decompress not lossless"
        );
        assert_eq!(
            decompressed.logs().len(),
            2,
            "receipt should retain both logs after roundtrip"
        );

        // Compact path with byte accounting.
        let mut buf = alloc::vec::Vec::new();
        let written = receipt.to_compact(&mut buf);
        assert_eq!(
            written,
            buf.len(),
            "receipt to_compact return value must equal bytes written"
        );
        let (decoded, rest) = ArbReceiptEnvelope::<Log>::from_compact(&buf, buf.len());
        assert_eq!(decoded, receipt, "ArbReceiptEnvelope Compact not lossless");
        assert!(
            rest.is_empty(),
            "receipt from_compact left {} trailing byte(s)",
            rest.len()
        );
    }
}
