use crate::block_info::{DecodeError, L1BlockInfoTx};
use alloy_eips::{eip2718::Eip2718Error, BlockNumHash};
use alloy_primitives::B256;
use op_alloy_consensus::{OpBlock, OpTxEnvelope, OpTxType};
use op_alloy_genesis::ChainGenesis;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Default)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct BlockInfo {
    pub hash: B256,
    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
    pub number: u64,
    pub parent_hash: B256,
    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
    pub timestamp: u64,
}
impl BlockInfo {
    pub const fn new(hash: B256, number: u64, parent_hash: B256, timestamp: u64) -> Self {
        Self { hash, number, parent_hash, timestamp }
    }
    pub const fn id(&self) -> BlockNumHash {
        BlockNumHash { hash: self.hash, number: self.number }
    }
}
impl From<OpBlock> for BlockInfo {
    fn from(block: OpBlock) -> Self {
        Self::from(&block)
    }
}
impl From<&OpBlock> for BlockInfo {
    fn from(block: &OpBlock) -> Self {
        Self {
            hash: block.header.hash_slow(),
            number: block.header.number,
            parent_hash: block.header.parent_hash,
            timestamp: block.header.timestamp,
        }
    }
}
impl core::fmt::Display for BlockInfo {
    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
        write!(
            f,
            "BlockInfo {{ hash: {}, number: {}, parent_hash: {}, timestamp: {} }}",
            self.hash, self.number, self.parent_hash, self.timestamp
        )
    }
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq, Default)]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct L2BlockInfo {
    pub block_info: BlockInfo,
    pub l1_origin: BlockNumHash,
    #[cfg_attr(feature = "serde", serde(with = "alloy_serde::quantity"))]
    pub seq_num: u64,
}
#[derive(Debug, derive_more::Display)]
pub enum FromBlockError {
    #[display("Invalid genesis hash")]
    InvalidGenesisHash,
    #[display("L2 block is missing L1 info deposit transaction ({_0})")]
    MissingL1InfoDeposit(B256),
    #[display("First payload transaction has unexpected type: {_0}")]
    UnexpectedTxType(u8),
    #[display("Failed to decode the first transaction into an OpTxEnvelope: {_0}")]
    TxEnvelopeDecodeError(Eip2718Error),
    #[display("First payload transaction is not a deposit transaction, type: {_0}")]
    FirstTxNonDeposit(u8),
    #[display("Failed to decode the L1BlockInfoTx from the deposit transaction: {_0}")]
    BlockInfoDecodeError(DecodeError),
}
#[cfg(feature = "std")]
impl std::error::Error for FromBlockError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::TxEnvelopeDecodeError(err) => Some(err),
            Self::BlockInfoDecodeError(err) => Some(err),
            _ => None,
        }
    }
}
#[cfg(not(feature = "std"))]
impl core::error::Error for FromBlockError {
    fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
        match self {
            Self::BlockInfoDecodeError(err) => Some(err),
            _ => None,
        }
    }
}
impl L2BlockInfo {
    pub const fn new(block_info: BlockInfo, l1_origin: BlockNumHash, seq_num: u64) -> Self {
        Self { block_info, l1_origin, seq_num }
    }
    pub fn from_block_and_genesis(
        block: &OpBlock,
        genesis: &ChainGenesis,
    ) -> Result<Self, FromBlockError> {
        let block_info = BlockInfo::from(block);
        let (l1_origin, sequence_number) = if block_info.number == genesis.l2.number {
            if block_info.hash != genesis.l2.hash {
                return Err(FromBlockError::InvalidGenesisHash);
            }
            (genesis.l1, 0)
        } else {
            if block.body.transactions.is_empty() {
                return Err(FromBlockError::MissingL1InfoDeposit(block_info.hash));
            }
            let tx = &block.body.transactions[0];
            if tx.tx_type() != OpTxType::Deposit {
                return Err(FromBlockError::UnexpectedTxType(tx.tx_type().into()));
            }
            let OpTxEnvelope::Deposit(tx) = tx else {
                return Err(FromBlockError::FirstTxNonDeposit(tx.tx_type().into()));
            };
            let l1_info = L1BlockInfoTx::decode_calldata(tx.input.as_ref())
                .map_err(FromBlockError::BlockInfoDecodeError)?;
            (l1_info.id(), l1_info.sequence_number())
        };
        Ok(Self { block_info, l1_origin, seq_num: sequence_number })
    }
}
#[cfg(test)]
#[cfg(feature = "serde")]
mod tests {
    use super::*;
    #[test]
    fn test_block_id_bounds() {
        let block_info = BlockInfo {
            hash: B256::from([1; 32]),
            number: 0,
            parent_hash: B256::from([2; 32]),
            timestamp: 1,
        };
        let expected = BlockNumHash { hash: B256::from([1; 32]), number: 0 };
        assert_eq!(block_info.id(), expected);
        let block_info = BlockInfo {
            hash: B256::from([1; 32]),
            number: u64::MAX,
            parent_hash: B256::from([2; 32]),
            timestamp: 1,
        };
        let expected = BlockNumHash { hash: B256::from([1; 32]), number: u64::MAX };
        assert_eq!(block_info.id(), expected);
    }
    #[test]
    fn test_deserialize_block_info() {
        let block_info = BlockInfo {
            hash: B256::from([1; 32]),
            number: 1,
            parent_hash: B256::from([2; 32]),
            timestamp: 1,
        };
        let json = r#"{
            "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
            "number": 1,
            "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
            "timestamp": 1
        }"#;
        let deserialized: BlockInfo = serde_json::from_str(json).unwrap();
        assert_eq!(deserialized, block_info);
    }
    #[test]
    fn test_deserialize_block_info_with_hex() {
        let block_info = BlockInfo {
            hash: B256::from([1; 32]),
            number: 1,
            parent_hash: B256::from([2; 32]),
            timestamp: 1,
        };
        let json = r#"{
            "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
            "number": "0x1",
            "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
            "timestamp": "0x1"
        }"#;
        let deserialized: BlockInfo = serde_json::from_str(json).unwrap();
        assert_eq!(deserialized, block_info);
    }
    #[test]
    fn test_deserialize_l2_block_info() {
        let l2_block_info = L2BlockInfo {
            block_info: BlockInfo {
                hash: B256::from([1; 32]),
                number: 1,
                parent_hash: B256::from([2; 32]),
                timestamp: 1,
            },
            l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 },
            seq_num: 3,
        };
        let json = r#"{
            "blockInfo": {
                "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
                "number": 1,
                "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
                "timestamp": 1
            },
            "l1Origin": {
                "hash": "0x0303030303030303030303030303030303030303030303030303030303030303",
                "number": 2
            },
            "seqNum": 3
        }"#;
        let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap();
        assert_eq!(deserialized, l2_block_info);
    }
    #[test]
    fn test_deserialize_l2_block_info_hex() {
        let l2_block_info = L2BlockInfo {
            block_info: BlockInfo {
                hash: B256::from([1; 32]),
                number: 1,
                parent_hash: B256::from([2; 32]),
                timestamp: 1,
            },
            l1_origin: BlockNumHash { hash: B256::from([3; 32]), number: 2 },
            seq_num: 3,
        };
        let json = r#"{
            "blockInfo": {
                "hash": "0x0101010101010101010101010101010101010101010101010101010101010101",
                "number": "0x1",
                "parentHash": "0x0202020202020202020202020202020202020202020202020202020202020202",
                "timestamp": "0x1"
            },
            "l1Origin": {
                "hash": "0x0303030303030303030303030303030303030303030303030303030303030303",
                "number": 2
            },
            "seqNum": "0x3"
        }"#;
        let deserialized: L2BlockInfo = serde_json::from_str(json).unwrap();
        assert_eq!(deserialized, l2_block_info);
    }
}