use super::methods::*;
use crate::rpc::codec::SSZSnappyInboundCodec;
use bls::Signature;
use futures::future::BoxFuture;
use futures::prelude::{AsyncRead, AsyncWrite};
use futures::{FutureExt, StreamExt};
use libp2p::core::{InboundUpgrade, UpgradeInfo};
use ssz::Encode;
use ssz_types::VariableList;
use std::io;
use std::marker::PhantomData;
use std::sync::{Arc, LazyLock};
use std::time::Duration;
use strum::{AsRefStr, Display, EnumIter, EnumString, IntoStaticStr};
use tokio_util::{
    codec::Framed,
    compat::{Compat, FuturesAsyncReadCompatExt},
};
use types::{
    BeaconBlock, BeaconBlockAltair, BeaconBlockBase, BlobSidecar, ChainSpec, DataColumnSidecarFulu,
    DataColumnSidecarGloas, EmptyBlock, Epoch, EthSpec, EthSpecId, ForkContext, ForkName,
    LightClientBootstrap, LightClientBootstrapAltair, LightClientFinalityUpdate,
    LightClientFinalityUpdateAltair, LightClientOptimisticUpdate,
    LightClientOptimisticUpdateAltair, LightClientUpdate, MainnetEthSpec, MinimalEthSpec,
    SignedBeaconBlock, SignedExecutionPayloadEnvelope,
};

// Note: Hardcoding the `EthSpec` type for `SignedBeaconBlock` as min/max values is
// same across different `EthSpec` implementations.
pub static SIGNED_BEACON_BLOCK_BASE_MIN: LazyLock<usize> = LazyLock::new(|| {
    SignedBeaconBlock::<MainnetEthSpec>::from_block(
        BeaconBlock::Base(BeaconBlockBase::<MainnetEthSpec>::empty(
            &MainnetEthSpec::default_spec(),
        )),
        Signature::empty(),
    )
    .as_ssz_bytes()
    .len()
});
pub static SIGNED_BEACON_BLOCK_BASE_MAX: LazyLock<usize> = LazyLock::new(|| {
    SignedBeaconBlock::<MainnetEthSpec>::from_block(
        BeaconBlock::Base(BeaconBlockBase::full(&MainnetEthSpec::default_spec())),
        Signature::empty(),
    )
    .as_ssz_bytes()
    .len()
});

pub static SIGNED_BEACON_BLOCK_ALTAIR_MAX: LazyLock<usize> = LazyLock::new(|| {
    SignedBeaconBlock::<MainnetEthSpec>::from_block(
        BeaconBlock::Altair(BeaconBlockAltair::full(&MainnetEthSpec::default_spec())),
        Signature::empty(),
    )
    .as_ssz_bytes()
    .len()
});

/// The `BeaconBlockBellatrix` block has an `ExecutionPayload` field which has a max size ~16 GiB for future proofing.
/// We calculate the value from its fields instead of constructing the block and checking the length.
/// Note: This is only the theoretical upper bound. We further bound the max size we receive over the network
/// with `max_payload_size`.
pub static SIGNED_BEACON_BLOCK_BELLATRIX_MAX: LazyLock<usize> =
    LazyLock::new(||     // Size of a full altair block
    *SIGNED_BEACON_BLOCK_ALTAIR_MAX
    + types::ExecutionPayload::<MainnetEthSpec>::max_execution_payload_bellatrix_size() // adding max size of execution payload (~16gb)
    + ssz::BYTES_PER_LENGTH_OFFSET); // Adding the additional ssz offset for the `ExecutionPayload` field

pub static SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MIN: LazyLock<usize> =
    LazyLock::new(SignedExecutionPayloadEnvelope::<MainnetEthSpec>::min_size);

pub static SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MAX: LazyLock<usize> =
    LazyLock::new(SignedExecutionPayloadEnvelope::<MainnetEthSpec>::max_size);

pub static BLOB_SIDECAR_SIZE: LazyLock<usize> =
    LazyLock::new(BlobSidecar::<MainnetEthSpec>::max_size);

pub static BLOB_SIDECAR_SIZE_MINIMAL: LazyLock<usize> =
    LazyLock::new(BlobSidecar::<MinimalEthSpec>::max_size);

pub static ERROR_TYPE_MIN: LazyLock<usize> = LazyLock::new(|| {
    VariableList::<u8, MaxErrorLen>::try_from(Vec::<u8>::new())
        .expect("MaxErrorLen should not exceed MAX_ERROR_LEN")
        .as_ssz_bytes()
        .len()
});

pub static ERROR_TYPE_MAX: LazyLock<usize> = LazyLock::new(|| {
    VariableList::<u8, MaxErrorLen>::try_from(vec![0u8; MAX_ERROR_LEN as usize])
        .expect("MaxErrorLen should not exceed MAX_ERROR_LEN")
        .as_ssz_bytes()
        .len()
});

pub static LIGHT_CLIENT_FINALITY_UPDATE_CAPELLA_MAX: LazyLock<usize> = LazyLock::new(|| {
    LightClientFinalityUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Capella)
});
pub static LIGHT_CLIENT_FINALITY_UPDATE_DENEB_MAX: LazyLock<usize> = LazyLock::new(|| {
    LightClientFinalityUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Deneb)
});
pub static LIGHT_CLIENT_FINALITY_UPDATE_ELECTRA_MAX: LazyLock<usize> = LazyLock::new(|| {
    LightClientFinalityUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Electra)
});
pub static LIGHT_CLIENT_OPTIMISTIC_UPDATE_CAPELLA_MAX: LazyLock<usize> = LazyLock::new(|| {
    LightClientOptimisticUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Capella)
});
pub static LIGHT_CLIENT_OPTIMISTIC_UPDATE_DENEB_MAX: LazyLock<usize> = LazyLock::new(|| {
    LightClientOptimisticUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Deneb)
});
pub static LIGHT_CLIENT_OPTIMISTIC_UPDATE_ELECTRA_MAX: LazyLock<usize> = LazyLock::new(|| {
    LightClientOptimisticUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Electra)
});
pub static LIGHT_CLIENT_BOOTSTRAP_CAPELLA_MAX: LazyLock<usize> = LazyLock::new(|| {
    LightClientBootstrap::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Capella)
});
pub static LIGHT_CLIENT_BOOTSTRAP_DENEB_MAX: LazyLock<usize> =
    LazyLock::new(|| LightClientBootstrap::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Deneb));
pub static LIGHT_CLIENT_BOOTSTRAP_ELECTRA_MAX: LazyLock<usize> = LazyLock::new(|| {
    LightClientBootstrap::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Electra)
});

pub static LIGHT_CLIENT_UPDATES_BY_RANGE_CAPELLA_MAX: LazyLock<usize> =
    LazyLock::new(|| LightClientUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Capella));
pub static LIGHT_CLIENT_UPDATES_BY_RANGE_DENEB_MAX: LazyLock<usize> =
    LazyLock::new(|| LightClientUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Deneb));
pub static LIGHT_CLIENT_UPDATES_BY_RANGE_ELECTRA_MAX: LazyLock<usize> =
    LazyLock::new(|| LightClientUpdate::<MainnetEthSpec>::ssz_max_len_for_fork(ForkName::Electra));

/// The protocol prefix the RPC protocol id.
const PROTOCOL_PREFIX: &str = "/eth2/beacon_chain/req";
/// The number of seconds to wait for the first bytes of a request once a protocol has been
/// established before the stream is terminated.
const REQUEST_TIMEOUT: u64 = 15;

/// Returns the rpc limits for beacon_block_by_range and beacon_block_by_root responses.
///
/// Note: This function should take care to return the min/max limits accounting for all
/// previous valid forks when adding a new fork variant.
pub fn rpc_block_limits_by_fork(current_fork: ForkName) -> RpcLimits {
    match &current_fork {
        ForkName::Base => {
            RpcLimits::new(*SIGNED_BEACON_BLOCK_BASE_MIN, *SIGNED_BEACON_BLOCK_BASE_MAX)
        }
        ForkName::Altair => RpcLimits::new(
            *SIGNED_BEACON_BLOCK_BASE_MIN, // Base block is smaller than altair blocks
            *SIGNED_BEACON_BLOCK_ALTAIR_MAX, // Altair block is larger than base blocks
        ),
        // After the merge the max SSZ size of a block is absurdly big. The size is actually
        // bound by other constants, so here we default to the bellatrix's max value
        // After the merge the max SSZ size includes the execution payload.
        // Gloas blocks no longer contain the execution payload, but we must
        // still accept pre-Gloas blocks during historical sync, so we keep the
        // Bellatrix max as the upper bound.
        ForkName::Bellatrix
        | ForkName::Capella
        | ForkName::Deneb
        | ForkName::Electra
        | ForkName::Fulu
        | ForkName::Gloas => RpcLimits::new(
            *SIGNED_BEACON_BLOCK_BASE_MIN,
            *SIGNED_BEACON_BLOCK_BELLATRIX_MAX,
        ),
    }
}

/// Returns the rpc limits for payload_envelope_by_range and payload_envelope_by_root responses.
pub fn rpc_payload_limits() -> RpcLimits {
    RpcLimits::new(
        *SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MIN,
        *SIGNED_EXECUTION_PAYLOAD_ENVELOPE_MAX,
    )
}

fn rpc_light_client_updates_by_range_limits_by_fork(current_fork: ForkName) -> RpcLimits {
    let altair_fixed_len = LightClientFinalityUpdateAltair::<MainnetEthSpec>::ssz_fixed_len();

    match &current_fork {
        ForkName::Base => RpcLimits::new(0, 0),
        ForkName::Altair | ForkName::Bellatrix => {
            RpcLimits::new(altair_fixed_len, altair_fixed_len)
        }
        ForkName::Capella => {
            RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_UPDATES_BY_RANGE_CAPELLA_MAX)
        }
        ForkName::Deneb => {
            RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_UPDATES_BY_RANGE_DENEB_MAX)
        }
        ForkName::Electra | ForkName::Fulu | ForkName::Gloas => {
            RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_UPDATES_BY_RANGE_ELECTRA_MAX)
        }
    }
}

fn rpc_light_client_finality_update_limits_by_fork(current_fork: ForkName) -> RpcLimits {
    let altair_fixed_len = LightClientFinalityUpdateAltair::<MainnetEthSpec>::ssz_fixed_len();

    match &current_fork {
        ForkName::Base => RpcLimits::new(0, 0),
        ForkName::Altair | ForkName::Bellatrix => {
            RpcLimits::new(altair_fixed_len, altair_fixed_len)
        }
        ForkName::Capella => {
            RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_CAPELLA_MAX)
        }
        ForkName::Deneb => {
            RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_DENEB_MAX)
        }
        ForkName::Electra | ForkName::Fulu | ForkName::Gloas => {
            RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_FINALITY_UPDATE_ELECTRA_MAX)
        }
    }
}

fn rpc_light_client_optimistic_update_limits_by_fork(current_fork: ForkName) -> RpcLimits {
    let altair_fixed_len = LightClientOptimisticUpdateAltair::<MainnetEthSpec>::ssz_fixed_len();

    match &current_fork {
        ForkName::Base => RpcLimits::new(0, 0),
        ForkName::Altair | ForkName::Bellatrix => {
            RpcLimits::new(altair_fixed_len, altair_fixed_len)
        }
        ForkName::Capella => RpcLimits::new(
            altair_fixed_len,
            *LIGHT_CLIENT_OPTIMISTIC_UPDATE_CAPELLA_MAX,
        ),
        ForkName::Deneb => {
            RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_OPTIMISTIC_UPDATE_DENEB_MAX)
        }
        ForkName::Electra | ForkName::Fulu | ForkName::Gloas => RpcLimits::new(
            altair_fixed_len,
            *LIGHT_CLIENT_OPTIMISTIC_UPDATE_ELECTRA_MAX,
        ),
    }
}

fn rpc_light_client_bootstrap_limits_by_fork(current_fork: ForkName) -> RpcLimits {
    let altair_fixed_len = LightClientBootstrapAltair::<MainnetEthSpec>::ssz_fixed_len();

    match &current_fork {
        ForkName::Base => RpcLimits::new(0, 0),
        ForkName::Altair | ForkName::Bellatrix => {
            RpcLimits::new(altair_fixed_len, altair_fixed_len)
        }
        ForkName::Capella => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_CAPELLA_MAX),
        ForkName::Deneb => RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_DENEB_MAX),
        ForkName::Electra | ForkName::Fulu | ForkName::Gloas => {
            RpcLimits::new(altair_fixed_len, *LIGHT_CLIENT_BOOTSTRAP_ELECTRA_MAX)
        }
    }
}

/// Protocol names to be used.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumString, AsRefStr, Display)]
#[strum(serialize_all = "snake_case")]
pub enum Protocol {
    /// The Status protocol name.
    Status,
    /// The Goodbye protocol name.
    Goodbye,
    /// The `BlocksByRange` protocol name.
    #[strum(serialize = "beacon_blocks_by_range")]
    BlocksByRange,
    /// The `BlocksByRoot` protocol name.
    #[strum(serialize = "beacon_blocks_by_root")]
    BlocksByRoot,
    /// The `BlocksByHead` protocol name.
    #[strum(serialize = "beacon_blocks_by_head")]
    BlocksByHead,
    /// The `BlobsByRange` protocol name.
    #[strum(serialize = "blob_sidecars_by_range")]
    BlobsByRange,
    /// The `ExecutionPayloadEnvelopesByRoot` protocol name.
    #[strum(serialize = "execution_payload_envelopes_by_root")]
    PayloadEnvelopesByRoot,
    /// The `ExecutionPayloadEnvelopesByRange` protocol name.
    #[strum(serialize = "execution_payload_envelopes_by_range")]
    PayloadEnvelopesByRange,
    /// The `BlobsByRoot` protocol name.
    #[strum(serialize = "blob_sidecars_by_root")]
    BlobsByRoot,
    /// The `DataColumnSidecarsByRoot` protocol name.
    #[strum(serialize = "data_column_sidecars_by_root")]
    DataColumnsByRoot,
    /// The `DataColumnSidecarsByRange` protocol name.
    #[strum(serialize = "data_column_sidecars_by_range")]
    DataColumnsByRange,
    /// The `Ping` protocol name.
    Ping,
    /// The `MetaData` protocol name.
    #[strum(serialize = "metadata")]
    MetaData,
    /// The `LightClientBootstrap` protocol name.
    #[strum(serialize = "light_client_bootstrap")]
    LightClientBootstrap,
    /// The `LightClientOptimisticUpdate` protocol name.
    #[strum(serialize = "light_client_optimistic_update")]
    LightClientOptimisticUpdate,
    /// The `LightClientFinalityUpdate` protocol name.
    #[strum(serialize = "light_client_finality_update")]
    LightClientFinalityUpdate,
    /// The `LightClientUpdatesByRange` protocol name
    #[strum(serialize = "light_client_updates_by_range")]
    LightClientUpdatesByRange,
}

impl Protocol {
    pub(crate) fn terminator(self) -> Option<ResponseTermination> {
        match self {
            Protocol::Status => None,
            Protocol::Goodbye => None,
            Protocol::BlocksByRange => Some(ResponseTermination::BlocksByRange),
            Protocol::BlocksByRoot => Some(ResponseTermination::BlocksByRoot),
            Protocol::BlocksByHead => Some(ResponseTermination::BlocksByHead),
            Protocol::PayloadEnvelopesByRange => Some(ResponseTermination::PayloadEnvelopesByRange),
            Protocol::PayloadEnvelopesByRoot => Some(ResponseTermination::PayloadEnvelopesByRoot),
            Protocol::BlobsByRange => Some(ResponseTermination::BlobsByRange),
            Protocol::BlobsByRoot => Some(ResponseTermination::BlobsByRoot),
            Protocol::DataColumnsByRoot => Some(ResponseTermination::DataColumnsByRoot),
            Protocol::DataColumnsByRange => Some(ResponseTermination::DataColumnsByRange),
            Protocol::Ping => None,
            Protocol::MetaData => None,
            Protocol::LightClientBootstrap => None,
            Protocol::LightClientOptimisticUpdate => None,
            Protocol::LightClientFinalityUpdate => None,
            Protocol::LightClientUpdatesByRange => None,
        }
    }
}

/// RPC Encondings supported.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Encoding {
    SSZSnappy,
}

/// All valid protocol name and version combinations.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, EnumIter)]
pub enum SupportedProtocol {
    StatusV1,
    StatusV2,
    GoodbyeV1,
    BlocksByRangeV1,
    BlocksByRangeV2,
    BlocksByRootV1,
    BlocksByRootV2,
    BlocksByHeadV1,
    PayloadEnvelopesByRangeV1,
    PayloadEnvelopesByRootV1,
    BlobsByRangeV1,
    BlobsByRootV1,
    DataColumnsByRootV1,
    DataColumnsByRangeV1,
    PingV1,
    MetaDataV1,
    MetaDataV2,
    MetaDataV3,
    LightClientBootstrapV1,
    LightClientOptimisticUpdateV1,
    LightClientFinalityUpdateV1,
    LightClientUpdatesByRangeV1,
}

impl SupportedProtocol {
    pub fn version_string(&self) -> &'static str {
        match self {
            SupportedProtocol::StatusV1 => "1",
            SupportedProtocol::StatusV2 => "2",
            SupportedProtocol::GoodbyeV1 => "1",
            SupportedProtocol::BlocksByRangeV1 => "1",
            SupportedProtocol::BlocksByRangeV2 => "2",
            SupportedProtocol::PayloadEnvelopesByRangeV1 => "1",
            SupportedProtocol::PayloadEnvelopesByRootV1 => "1",
            SupportedProtocol::BlocksByRootV1 => "1",
            SupportedProtocol::BlocksByRootV2 => "2",
            SupportedProtocol::BlocksByHeadV1 => "1",
            SupportedProtocol::BlobsByRangeV1 => "1",
            SupportedProtocol::BlobsByRootV1 => "1",
            SupportedProtocol::DataColumnsByRootV1 => "1",
            SupportedProtocol::DataColumnsByRangeV1 => "1",
            SupportedProtocol::PingV1 => "1",
            SupportedProtocol::MetaDataV1 => "1",
            SupportedProtocol::MetaDataV2 => "2",
            SupportedProtocol::MetaDataV3 => "3",
            SupportedProtocol::LightClientBootstrapV1 => "1",
            SupportedProtocol::LightClientOptimisticUpdateV1 => "1",
            SupportedProtocol::LightClientFinalityUpdateV1 => "1",
            SupportedProtocol::LightClientUpdatesByRangeV1 => "1",
        }
    }

    pub fn protocol(&self) -> Protocol {
        match self {
            SupportedProtocol::StatusV1 => Protocol::Status,
            SupportedProtocol::StatusV2 => Protocol::Status,
            SupportedProtocol::GoodbyeV1 => Protocol::Goodbye,
            SupportedProtocol::BlocksByRangeV1 => Protocol::BlocksByRange,
            SupportedProtocol::BlocksByRangeV2 => Protocol::BlocksByRange,
            SupportedProtocol::BlocksByRootV1 => Protocol::BlocksByRoot,
            SupportedProtocol::BlocksByRootV2 => Protocol::BlocksByRoot,
            SupportedProtocol::BlocksByHeadV1 => Protocol::BlocksByHead,
            SupportedProtocol::PayloadEnvelopesByRangeV1 => Protocol::PayloadEnvelopesByRange,
            SupportedProtocol::PayloadEnvelopesByRootV1 => Protocol::PayloadEnvelopesByRoot,
            SupportedProtocol::BlobsByRangeV1 => Protocol::BlobsByRange,
            SupportedProtocol::BlobsByRootV1 => Protocol::BlobsByRoot,
            SupportedProtocol::DataColumnsByRootV1 => Protocol::DataColumnsByRoot,
            SupportedProtocol::DataColumnsByRangeV1 => Protocol::DataColumnsByRange,
            SupportedProtocol::PingV1 => Protocol::Ping,
            SupportedProtocol::MetaDataV1 => Protocol::MetaData,
            SupportedProtocol::MetaDataV2 => Protocol::MetaData,
            SupportedProtocol::MetaDataV3 => Protocol::MetaData,
            SupportedProtocol::LightClientBootstrapV1 => Protocol::LightClientBootstrap,
            SupportedProtocol::LightClientOptimisticUpdateV1 => {
                Protocol::LightClientOptimisticUpdate
            }
            SupportedProtocol::LightClientFinalityUpdateV1 => Protocol::LightClientFinalityUpdate,
            SupportedProtocol::LightClientUpdatesByRangeV1 => Protocol::LightClientUpdatesByRange,
        }
    }

    fn currently_supported(fork_context: &ForkContext) -> Vec<ProtocolId> {
        let mut supported = vec![
            ProtocolId::new(Self::StatusV2, Encoding::SSZSnappy),
            ProtocolId::new(Self::StatusV1, Encoding::SSZSnappy),
            ProtocolId::new(Self::GoodbyeV1, Encoding::SSZSnappy),
            // V2 variants have higher preference then V1
            ProtocolId::new(Self::BlocksByRangeV2, Encoding::SSZSnappy),
            ProtocolId::new(Self::BlocksByRangeV1, Encoding::SSZSnappy),
            ProtocolId::new(Self::BlocksByRootV2, Encoding::SSZSnappy),
            ProtocolId::new(Self::BlocksByRootV1, Encoding::SSZSnappy),
            ProtocolId::new(Self::PingV1, Encoding::SSZSnappy),
        ];
        if fork_context.spec.is_peer_das_scheduled() {
            supported.extend_from_slice(&[
                // V3 variants have higher preference for protocol negotation
                ProtocolId::new(Self::MetaDataV3, Encoding::SSZSnappy),
                ProtocolId::new(Self::MetaDataV2, Encoding::SSZSnappy),
                ProtocolId::new(Self::MetaDataV1, Encoding::SSZSnappy),
            ]);
        } else {
            supported.extend_from_slice(&[
                ProtocolId::new(Self::MetaDataV2, Encoding::SSZSnappy),
                ProtocolId::new(Self::MetaDataV1, Encoding::SSZSnappy),
            ]);
        }
        if fork_context.fork_exists(ForkName::Deneb) {
            supported.extend_from_slice(&[
                ProtocolId::new(SupportedProtocol::BlobsByRootV1, Encoding::SSZSnappy),
                ProtocolId::new(SupportedProtocol::BlobsByRangeV1, Encoding::SSZSnappy),
            ]);
        }
        if fork_context.spec.is_peer_das_scheduled() {
            supported.extend_from_slice(&[
                ProtocolId::new(SupportedProtocol::DataColumnsByRootV1, Encoding::SSZSnappy),
                ProtocolId::new(SupportedProtocol::DataColumnsByRangeV1, Encoding::SSZSnappy),
            ]);
        }
        if fork_context.fork_exists(ForkName::Gloas) {
            supported.extend_from_slice(&[
                ProtocolId::new(
                    SupportedProtocol::PayloadEnvelopesByRangeV1,
                    Encoding::SSZSnappy,
                ),
                ProtocolId::new(
                    SupportedProtocol::PayloadEnvelopesByRootV1,
                    Encoding::SSZSnappy,
                ),
            ]);
        }
        // BeaconBlocksByHead is new in Fulu (consensus-specs PR 5181).
        if fork_context.fork_exists(ForkName::Fulu) {
            supported.push(ProtocolId::new(
                SupportedProtocol::BlocksByHeadV1,
                Encoding::SSZSnappy,
            ));
        }
        supported
    }
}

impl std::fmt::Display for Encoding {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let repr = match self {
            Encoding::SSZSnappy => "ssz_snappy",
        };
        f.write_str(repr)
    }
}

#[derive(Debug, Clone)]
pub struct RPCProtocol<E: EthSpec> {
    pub fork_context: Arc<ForkContext>,
    pub max_rpc_size: usize,
    pub enable_light_client_server: bool,
    pub phantom: PhantomData<E>,
}

impl<E: EthSpec> UpgradeInfo for RPCProtocol<E> {
    type Info = ProtocolId;
    type InfoIter = Vec<Self::Info>;

    /// The list of supported RPC protocols for Lighthouse.
    fn protocol_info(&self) -> Self::InfoIter {
        let mut supported_protocols = SupportedProtocol::currently_supported(&self.fork_context);
        if self.enable_light_client_server {
            supported_protocols.push(ProtocolId::new(
                SupportedProtocol::LightClientBootstrapV1,
                Encoding::SSZSnappy,
            ));
            supported_protocols.push(ProtocolId::new(
                SupportedProtocol::LightClientOptimisticUpdateV1,
                Encoding::SSZSnappy,
            ));
            supported_protocols.push(ProtocolId::new(
                SupportedProtocol::LightClientFinalityUpdateV1,
                Encoding::SSZSnappy,
            ));
            supported_protocols.push(ProtocolId::new(
                SupportedProtocol::LightClientUpdatesByRangeV1,
                Encoding::SSZSnappy,
            ));
        }
        supported_protocols
    }
}

/// Represents the ssz length bounds for RPC messages.
#[derive(Debug, PartialEq)]
pub struct RpcLimits {
    pub min: usize,
    pub max: usize,
}

impl RpcLimits {
    pub fn new(min: usize, max: usize) -> Self {
        Self { min, max }
    }

    /// Returns true if the given length is greater than `max_rpc_size` or out of
    /// bounds for the given ssz type, returns false otherwise.
    pub fn is_out_of_bounds(&self, length: usize, max_rpc_size: usize) -> bool {
        length > std::cmp::min(self.max, max_rpc_size) || length < self.min
    }
}

/// Tracks the types in a protocol id.
#[derive(Clone, Debug)]
pub struct ProtocolId {
    /// The protocol name and version
    pub versioned_protocol: SupportedProtocol,

    /// The encoding of the RPC.
    pub encoding: Encoding,

    /// The protocol id that is formed from the above fields.
    protocol_id: String,
}

impl AsRef<str> for ProtocolId {
    fn as_ref(&self) -> &str {
        self.protocol_id.as_ref()
    }
}

impl ProtocolId {
    /// Returns min and max size for messages of given protocol id requests.
    pub fn rpc_request_limits<E: EthSpec>(&self, spec: &ChainSpec) -> RpcLimits {
        match self.versioned_protocol.protocol() {
            Protocol::Status => RpcLimits::new(
                <StatusMessageV1 as Encode>::ssz_fixed_len(),
                <StatusMessageV2 as Encode>::ssz_fixed_len(),
            ),
            Protocol::Goodbye => RpcLimits::new(
                <GoodbyeReason as Encode>::ssz_fixed_len(),
                <GoodbyeReason as Encode>::ssz_fixed_len(),
            ),
            // V1 and V2 requests are the same
            Protocol::BlocksByRange => RpcLimits::new(
                <OldBlocksByRangeRequestV2 as Encode>::ssz_fixed_len(),
                <OldBlocksByRangeRequestV2 as Encode>::ssz_fixed_len(),
            ),
            Protocol::BlocksByRoot => RpcLimits::new(0, spec.max_blocks_by_root_request),
            Protocol::BlocksByHead => RpcLimits::new(
                <BlocksByHeadRequest as Encode>::ssz_fixed_len(),
                <BlocksByHeadRequest as Encode>::ssz_fixed_len(),
            ),
            Protocol::PayloadEnvelopesByRange => RpcLimits::new(
                <PayloadEnvelopesByRangeRequest as Encode>::ssz_fixed_len(),
                <PayloadEnvelopesByRangeRequest as Encode>::ssz_fixed_len(),
            ),
            Protocol::PayloadEnvelopesByRoot => {
                RpcLimits::new(0, spec.max_payload_envelopes_by_root_request)
            }
            Protocol::BlobsByRange => RpcLimits::new(
                <BlobsByRangeRequest as Encode>::ssz_fixed_len(),
                <BlobsByRangeRequest as Encode>::ssz_fixed_len(),
            ),
            Protocol::BlobsByRoot => RpcLimits::new(0, spec.max_blobs_by_root_request),
            Protocol::DataColumnsByRoot => RpcLimits::new(0, spec.max_data_columns_by_root_request),
            Protocol::DataColumnsByRange => RpcLimits::new(
                DataColumnsByRangeRequest::ssz_min_len(),
                DataColumnsByRangeRequest::ssz_max_len::<E>(),
            ),
            Protocol::Ping => RpcLimits::new(
                <Ping as Encode>::ssz_fixed_len(),
                <Ping as Encode>::ssz_fixed_len(),
            ),
            Protocol::LightClientBootstrap => RpcLimits::new(
                <LightClientBootstrapRequest as Encode>::ssz_fixed_len(),
                <LightClientBootstrapRequest as Encode>::ssz_fixed_len(),
            ),
            Protocol::LightClientOptimisticUpdate => RpcLimits::new(0, 0),
            Protocol::LightClientFinalityUpdate => RpcLimits::new(0, 0),
            Protocol::LightClientUpdatesByRange => RpcLimits::new(
                LightClientUpdatesByRangeRequest::ssz_min_len(),
                LightClientUpdatesByRangeRequest::ssz_max_len(),
            ),
            Protocol::MetaData => RpcLimits::new(0, 0), // Metadata requests are empty
        }
    }

    /// Returns min and max size for messages of given protocol id responses.
    pub fn rpc_response_limits<E: EthSpec>(&self, fork_context: &ForkContext) -> RpcLimits {
        match self.versioned_protocol.protocol() {
            Protocol::Status => RpcLimits::new(
                <StatusMessageV1 as Encode>::ssz_fixed_len(),
                <StatusMessageV2 as Encode>::ssz_fixed_len(),
            ),
            Protocol::Goodbye => RpcLimits::new(0, 0), // Goodbye request has no response
            Protocol::BlocksByRange => rpc_block_limits_by_fork(fork_context.current_fork_name()),
            Protocol::BlocksByRoot => rpc_block_limits_by_fork(fork_context.current_fork_name()),
            Protocol::BlocksByHead => rpc_block_limits_by_fork(fork_context.current_fork_name()),
            Protocol::PayloadEnvelopesByRange => rpc_payload_limits(),
            Protocol::PayloadEnvelopesByRoot => rpc_payload_limits(),
            Protocol::BlobsByRange => rpc_blob_limits::<E>(),
            Protocol::BlobsByRoot => rpc_blob_limits::<E>(),
            Protocol::DataColumnsByRoot => {
                rpc_data_column_limits::<E>(fork_context.current_fork_epoch(), &fork_context.spec)
            }
            Protocol::DataColumnsByRange => {
                rpc_data_column_limits::<E>(fork_context.current_fork_epoch(), &fork_context.spec)
            }
            Protocol::Ping => RpcLimits::new(
                <Ping as Encode>::ssz_fixed_len(),
                <Ping as Encode>::ssz_fixed_len(),
            ),
            Protocol::MetaData => RpcLimits::new(
                <MetaDataV1<E> as Encode>::ssz_fixed_len(),
                <MetaDataV3<E> as Encode>::ssz_fixed_len(),
            ),
            Protocol::LightClientBootstrap => {
                rpc_light_client_bootstrap_limits_by_fork(fork_context.current_fork_name())
            }
            Protocol::LightClientOptimisticUpdate => {
                rpc_light_client_optimistic_update_limits_by_fork(fork_context.current_fork_name())
            }
            Protocol::LightClientFinalityUpdate => {
                rpc_light_client_finality_update_limits_by_fork(fork_context.current_fork_name())
            }
            Protocol::LightClientUpdatesByRange => {
                rpc_light_client_updates_by_range_limits_by_fork(fork_context.current_fork_name())
            }
        }
    }

    /// Returns `true` if the given `ProtocolId` should expect `context_bytes` in the
    /// beginning of the stream, else returns `false`.
    pub fn has_context_bytes(&self) -> bool {
        match self.versioned_protocol {
            SupportedProtocol::BlocksByRangeV2
            | SupportedProtocol::BlocksByRootV2
            | SupportedProtocol::BlocksByHeadV1
            | SupportedProtocol::PayloadEnvelopesByRangeV1
            | SupportedProtocol::PayloadEnvelopesByRootV1
            | SupportedProtocol::BlobsByRangeV1
            | SupportedProtocol::BlobsByRootV1
            | SupportedProtocol::DataColumnsByRootV1
            | SupportedProtocol::DataColumnsByRangeV1
            | SupportedProtocol::LightClientBootstrapV1
            | SupportedProtocol::LightClientOptimisticUpdateV1
            | SupportedProtocol::LightClientFinalityUpdateV1
            | SupportedProtocol::LightClientUpdatesByRangeV1 => true,
            SupportedProtocol::StatusV1
            | SupportedProtocol::StatusV2
            | SupportedProtocol::BlocksByRootV1
            | SupportedProtocol::BlocksByRangeV1
            | SupportedProtocol::PingV1
            | SupportedProtocol::MetaDataV1
            | SupportedProtocol::MetaDataV2
            | SupportedProtocol::MetaDataV3
            | SupportedProtocol::GoodbyeV1 => false,
        }
    }
}

/// An RPC protocol ID.
impl ProtocolId {
    pub fn new(versioned_protocol: SupportedProtocol, encoding: Encoding) -> Self {
        let protocol_id = format!(
            "{}/{}/{}/{}",
            PROTOCOL_PREFIX,
            versioned_protocol.protocol(),
            versioned_protocol.version_string(),
            encoding
        );

        ProtocolId {
            versioned_protocol,
            encoding,
            protocol_id,
        }
    }
}

pub fn rpc_blob_limits<E: EthSpec>() -> RpcLimits {
    match E::spec_name() {
        EthSpecId::Minimal => {
            RpcLimits::new(*BLOB_SIDECAR_SIZE_MINIMAL, *BLOB_SIDECAR_SIZE_MINIMAL)
        }
        EthSpecId::Mainnet | EthSpecId::Gnosis => {
            RpcLimits::new(*BLOB_SIDECAR_SIZE, *BLOB_SIDECAR_SIZE)
        }
    }
}

pub fn rpc_data_column_limits<E: EthSpec>(
    current_digest_epoch: Epoch,
    spec: &ChainSpec,
) -> RpcLimits {
    let fork_name = spec.fork_name_at_epoch(current_digest_epoch);
    let max_blobs = spec.max_blobs_per_block(current_digest_epoch) as usize;

    if fork_name.gloas_enabled() {
        RpcLimits::new(
            DataColumnSidecarGloas::<E>::min_size(),
            DataColumnSidecarFulu::<E>::max_size(max_blobs),
        )
    } else {
        RpcLimits::new(
            DataColumnSidecarFulu::<E>::min_size(),
            DataColumnSidecarFulu::<E>::max_size(max_blobs),
        )
    }
}

/* Inbound upgrade */

// The inbound protocol reads the request, decodes it and returns the stream to the protocol
// handler to respond to once ready.

pub type InboundOutput<TSocket, E> = (RequestType<E>, InboundFramed<TSocket, E>);
pub type InboundFramed<TSocket, E> =
    Framed<std::pin::Pin<Box<Compat<TSocket>>>, SSZSnappyInboundCodec<E>>;

impl<TSocket, E> InboundUpgrade<TSocket> for RPCProtocol<E>
where
    TSocket: AsyncRead + AsyncWrite + Unpin + Send + 'static,
    E: EthSpec,
{
    type Output = InboundOutput<TSocket, E>;
    type Error = (Protocol, RPCError);
    type Future = BoxFuture<'static, Result<Self::Output, Self::Error>>;

    fn upgrade_inbound(self, socket: TSocket, protocol: ProtocolId) -> Self::Future {
        async move {
            let versioned_protocol = protocol.versioned_protocol;
            // convert the socket to tokio compatible socket
            let socket = socket.compat();
            let codec = match protocol.encoding {
                Encoding::SSZSnappy => SSZSnappyInboundCodec::new(
                    protocol,
                    self.max_rpc_size,
                    self.fork_context.clone(),
                ),
            };

            let socket = Framed::new(Box::pin(socket), codec);

            // MetaData requests should be empty, return the stream
            match versioned_protocol {
                SupportedProtocol::MetaDataV1 => {
                    Ok((RequestType::MetaData(MetadataRequest::new_v1()), socket))
                }
                SupportedProtocol::MetaDataV2 => {
                    Ok((RequestType::MetaData(MetadataRequest::new_v2()), socket))
                }
                SupportedProtocol::MetaDataV3 => {
                    Ok((RequestType::MetaData(MetadataRequest::new_v3()), socket))
                }
                SupportedProtocol::LightClientOptimisticUpdateV1 => {
                    Ok((RequestType::LightClientOptimisticUpdate, socket))
                }
                SupportedProtocol::LightClientFinalityUpdateV1 => {
                    Ok((RequestType::LightClientFinalityUpdate, socket))
                }
                _ => {
                    match tokio::time::timeout(
                        Duration::from_secs(REQUEST_TIMEOUT),
                        socket.into_future(),
                    )
                    .await
                    {
                        Err(e) => Err((versioned_protocol.protocol(), RPCError::from(e))),
                        Ok((Some(Ok(request)), stream)) => Ok((request, stream)),
                        Ok((Some(Err(e)), _)) => Err((versioned_protocol.protocol(), e)),
                        Ok((None, _)) => {
                            Err((versioned_protocol.protocol(), RPCError::IncompleteStream))
                        }
                    }
                }
            }
        }
        .boxed()
    }
}

#[derive(Debug, Clone, PartialEq, IntoStaticStr)]
pub enum RequestType<E: EthSpec> {
    Status(StatusMessage),
    Goodbye(GoodbyeReason),
    BlocksByRange(OldBlocksByRangeRequest),
    BlocksByRoot(BlocksByRootRequest),
    BlocksByHead(BlocksByHeadRequest),
    PayloadEnvelopesByRange(PayloadEnvelopesByRangeRequest),
    PayloadEnvelopesByRoot(PayloadEnvelopesByRootRequest),
    BlobsByRange(BlobsByRangeRequest),
    BlobsByRoot(BlobsByRootRequest),
    DataColumnsByRoot(DataColumnsByRootRequest<E>),
    DataColumnsByRange(DataColumnsByRangeRequest),
    LightClientBootstrap(LightClientBootstrapRequest),
    LightClientOptimisticUpdate,
    LightClientFinalityUpdate,
    LightClientUpdatesByRange(LightClientUpdatesByRangeRequest),
    Ping(Ping),
    MetaData(MetadataRequest<E>),
}

/// Implements the encoding per supported protocol for `RPCRequest`.
impl<E: EthSpec> RequestType<E> {
    /* These functions are used in the handler for stream management */

    /// Maximum number of responses expected for this request.
    pub fn max_responses(&self, digest_epoch: Epoch, spec: &ChainSpec) -> u64 {
        match self {
            RequestType::Status(_) => 1,
            RequestType::Goodbye(_) => 0,
            RequestType::BlocksByRange(req) => *req.count(),
            RequestType::BlocksByRoot(req) => req.block_roots().len() as u64,
            RequestType::BlocksByHead(req) => req.count,
            RequestType::PayloadEnvelopesByRange(req) => req.count,
            RequestType::PayloadEnvelopesByRoot(req) => req.beacon_block_roots.len() as u64,
            RequestType::BlobsByRange(req) => req.max_blobs_requested(digest_epoch, spec),
            RequestType::BlobsByRoot(req) => req.blob_ids.len() as u64,
            RequestType::DataColumnsByRoot(req) => req.max_requested() as u64,
            RequestType::DataColumnsByRange(req) => req.max_requested::<E>(),
            RequestType::Ping(_) => 1,
            RequestType::MetaData(_) => 1,
            RequestType::LightClientBootstrap(_) => 1,
            RequestType::LightClientOptimisticUpdate => 1,
            RequestType::LightClientFinalityUpdate => 1,
            RequestType::LightClientUpdatesByRange(req) => req.count,
        }
    }

    /// Gives the corresponding `SupportedProtocol` to this request.
    pub fn versioned_protocol(&self) -> SupportedProtocol {
        match self {
            RequestType::Status(req) => match req {
                StatusMessage::V1(_) => SupportedProtocol::StatusV1,
                StatusMessage::V2(_) => SupportedProtocol::StatusV2,
            },
            RequestType::Goodbye(_) => SupportedProtocol::GoodbyeV1,
            RequestType::BlocksByRange(req) => match req {
                OldBlocksByRangeRequest::V1(_) => SupportedProtocol::BlocksByRangeV1,
                OldBlocksByRangeRequest::V2(_) => SupportedProtocol::BlocksByRangeV2,
            },
            RequestType::BlocksByRoot(req) => match req {
                BlocksByRootRequest::V1(_) => SupportedProtocol::BlocksByRootV1,
                BlocksByRootRequest::V2(_) => SupportedProtocol::BlocksByRootV2,
            },
            RequestType::BlocksByHead(_) => SupportedProtocol::BlocksByHeadV1,
            RequestType::PayloadEnvelopesByRange(_) => SupportedProtocol::PayloadEnvelopesByRangeV1,
            RequestType::PayloadEnvelopesByRoot(_) => SupportedProtocol::PayloadEnvelopesByRootV1,
            RequestType::BlobsByRange(_) => SupportedProtocol::BlobsByRangeV1,
            RequestType::BlobsByRoot(_) => SupportedProtocol::BlobsByRootV1,
            RequestType::DataColumnsByRoot(_) => SupportedProtocol::DataColumnsByRootV1,
            RequestType::DataColumnsByRange(_) => SupportedProtocol::DataColumnsByRangeV1,
            RequestType::Ping(_) => SupportedProtocol::PingV1,
            RequestType::MetaData(req) => match req {
                MetadataRequest::V1(_) => SupportedProtocol::MetaDataV1,
                MetadataRequest::V2(_) => SupportedProtocol::MetaDataV2,
                MetadataRequest::V3(_) => SupportedProtocol::MetaDataV3,
            },
            RequestType::LightClientBootstrap(_) => SupportedProtocol::LightClientBootstrapV1,
            RequestType::LightClientOptimisticUpdate => {
                SupportedProtocol::LightClientOptimisticUpdateV1
            }
            RequestType::LightClientFinalityUpdate => {
                SupportedProtocol::LightClientFinalityUpdateV1
            }
            RequestType::LightClientUpdatesByRange(_) => {
                SupportedProtocol::LightClientUpdatesByRangeV1
            }
        }
    }

    /// Returns the `ResponseTermination` type associated with the request if a stream gets
    /// terminated.
    pub fn stream_termination(&self) -> ResponseTermination {
        match self {
            // this only gets called after `multiple_responses()` returns true. Therefore, only
            // variants that have `multiple_responses()` can have values.
            RequestType::BlocksByRange(_) => ResponseTermination::BlocksByRange,
            RequestType::BlocksByRoot(_) => ResponseTermination::BlocksByRoot,
            RequestType::BlocksByHead(_) => ResponseTermination::BlocksByHead,
            RequestType::PayloadEnvelopesByRange(_) => ResponseTermination::PayloadEnvelopesByRange,
            RequestType::PayloadEnvelopesByRoot(_) => ResponseTermination::PayloadEnvelopesByRoot,
            RequestType::BlobsByRange(_) => ResponseTermination::BlobsByRange,
            RequestType::BlobsByRoot(_) => ResponseTermination::BlobsByRoot,
            RequestType::DataColumnsByRoot(_) => ResponseTermination::DataColumnsByRoot,
            RequestType::DataColumnsByRange(_) => ResponseTermination::DataColumnsByRange,
            RequestType::Status(_) => unreachable!(),
            RequestType::Goodbye(_) => unreachable!(),
            RequestType::Ping(_) => unreachable!(),
            RequestType::MetaData(_) => unreachable!(),
            RequestType::LightClientBootstrap(_) => unreachable!(),
            RequestType::LightClientFinalityUpdate => unreachable!(),
            RequestType::LightClientOptimisticUpdate => unreachable!(),
            RequestType::LightClientUpdatesByRange(_) => unreachable!(),
        }
    }

    pub fn supported_protocols(&self) -> Vec<ProtocolId> {
        match self {
            // add more protocols when versions/encodings are supported
            RequestType::Status(_) => vec![
                ProtocolId::new(SupportedProtocol::StatusV2, Encoding::SSZSnappy),
                ProtocolId::new(SupportedProtocol::StatusV1, Encoding::SSZSnappy),
            ],
            RequestType::Goodbye(_) => vec![ProtocolId::new(
                SupportedProtocol::GoodbyeV1,
                Encoding::SSZSnappy,
            )],
            RequestType::BlocksByRange(_) => vec![
                ProtocolId::new(SupportedProtocol::BlocksByRangeV2, Encoding::SSZSnappy),
                ProtocolId::new(SupportedProtocol::BlocksByRangeV1, Encoding::SSZSnappy),
            ],
            RequestType::BlocksByRoot(_) => vec![
                ProtocolId::new(SupportedProtocol::BlocksByRootV2, Encoding::SSZSnappy),
                ProtocolId::new(SupportedProtocol::BlocksByRootV1, Encoding::SSZSnappy),
            ],
            RequestType::BlocksByHead(_) => vec![ProtocolId::new(
                SupportedProtocol::BlocksByHeadV1,
                Encoding::SSZSnappy,
            )],
            RequestType::PayloadEnvelopesByRange(_) => vec![ProtocolId::new(
                SupportedProtocol::PayloadEnvelopesByRangeV1,
                Encoding::SSZSnappy,
            )],
            RequestType::PayloadEnvelopesByRoot(_) => vec![ProtocolId::new(
                SupportedProtocol::PayloadEnvelopesByRootV1,
                Encoding::SSZSnappy,
            )],
            RequestType::BlobsByRange(_) => vec![ProtocolId::new(
                SupportedProtocol::BlobsByRangeV1,
                Encoding::SSZSnappy,
            )],
            RequestType::BlobsByRoot(_) => vec![ProtocolId::new(
                SupportedProtocol::BlobsByRootV1,
                Encoding::SSZSnappy,
            )],
            RequestType::DataColumnsByRoot(_) => vec![ProtocolId::new(
                SupportedProtocol::DataColumnsByRootV1,
                Encoding::SSZSnappy,
            )],
            RequestType::DataColumnsByRange(_) => vec![ProtocolId::new(
                SupportedProtocol::DataColumnsByRangeV1,
                Encoding::SSZSnappy,
            )],
            RequestType::Ping(_) => vec![ProtocolId::new(
                SupportedProtocol::PingV1,
                Encoding::SSZSnappy,
            )],
            RequestType::MetaData(_) => vec![
                ProtocolId::new(SupportedProtocol::MetaDataV3, Encoding::SSZSnappy),
                ProtocolId::new(SupportedProtocol::MetaDataV2, Encoding::SSZSnappy),
                ProtocolId::new(SupportedProtocol::MetaDataV1, Encoding::SSZSnappy),
            ],
            RequestType::LightClientBootstrap(_) => vec![ProtocolId::new(
                SupportedProtocol::LightClientBootstrapV1,
                Encoding::SSZSnappy,
            )],
            RequestType::LightClientOptimisticUpdate => vec![ProtocolId::new(
                SupportedProtocol::LightClientOptimisticUpdateV1,
                Encoding::SSZSnappy,
            )],
            RequestType::LightClientFinalityUpdate => vec![ProtocolId::new(
                SupportedProtocol::LightClientFinalityUpdateV1,
                Encoding::SSZSnappy,
            )],
            RequestType::LightClientUpdatesByRange(_) => vec![ProtocolId::new(
                SupportedProtocol::LightClientUpdatesByRangeV1,
                Encoding::SSZSnappy,
            )],
        }
    }

    pub fn expect_exactly_one_response(&self) -> bool {
        match self {
            RequestType::Status(_) => true,
            RequestType::Goodbye(_) => false,
            RequestType::BlocksByRange(_) => false,
            RequestType::BlocksByRoot(_) => false,
            RequestType::BlocksByHead(_) => false,
            RequestType::BlobsByRange(_) => false,
            RequestType::PayloadEnvelopesByRange(_) => false,
            RequestType::PayloadEnvelopesByRoot(_) => false,
            RequestType::BlobsByRoot(_) => false,
            RequestType::DataColumnsByRoot(_) => false,
            RequestType::DataColumnsByRange(_) => false,
            RequestType::Ping(_) => true,
            RequestType::MetaData(_) => true,
            RequestType::LightClientBootstrap(_) => true,
            RequestType::LightClientOptimisticUpdate => true,
            RequestType::LightClientFinalityUpdate => true,
            RequestType::LightClientUpdatesByRange(_) => true,
        }
    }
}

/// Error in RPC Encoding/Decoding.
#[derive(Debug, Clone, PartialEq, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum RPCError {
    /// Error when decoding the raw buffer from ssz.
    // NOTE: in the future a ssz::DecodeError should map to an InvalidData error
    #[strum(serialize = "decode_error")]
    SSZDecodeError(ssz::DecodeError),
    /// IO Error.
    IoError(String),
    /// The peer returned a valid response but the response indicated an error.
    ErrorResponse(RpcErrorResponse, String),
    /// Timed out waiting for a response.
    StreamTimeout,
    /// Peer does not support the protocol.
    UnsupportedProtocol,
    /// Stream ended unexpectedly.
    IncompleteStream,
    /// Peer sent invalid data.
    InvalidData(String),
    /// An error occurred due to internal reasons. Ex: timer failure.
    InternalError(&'static str),
    /// Negotiation with this peer timed out.
    NegotiationTimeout,
    /// Handler rejected this request.
    HandlerRejected,
    /// We have intentionally disconnected.
    Disconnected,
}

impl From<ssz::DecodeError> for RPCError {
    #[inline]
    fn from(err: ssz::DecodeError) -> Self {
        RPCError::SSZDecodeError(err)
    }
}
impl From<tokio::time::error::Elapsed> for RPCError {
    fn from(_: tokio::time::error::Elapsed) -> Self {
        RPCError::StreamTimeout
    }
}

impl From<io::Error> for RPCError {
    fn from(err: io::Error) -> Self {
        RPCError::IoError(err.to_string())
    }
}

// Error trait is required for `ProtocolsHandler`
impl std::fmt::Display for RPCError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match *self {
            RPCError::SSZDecodeError(ref err) => write!(f, "Error while decoding ssz: {:?}", err),
            RPCError::InvalidData(ref err) => write!(f, "Peer sent unexpected data: {}", err),
            RPCError::IoError(ref err) => write!(f, "IO Error: {}", err),
            RPCError::ErrorResponse(ref code, ref reason) => write!(
                f,
                "RPC response was an error: {} with reason: {}",
                code, reason
            ),
            RPCError::StreamTimeout => write!(f, "Stream Timeout"),
            RPCError::UnsupportedProtocol => write!(f, "Peer does not support the protocol"),
            RPCError::IncompleteStream => write!(f, "Stream ended unexpectedly"),
            RPCError::InternalError(ref err) => write!(f, "Internal error: {}", err),
            RPCError::NegotiationTimeout => write!(f, "Negotiation timeout"),
            RPCError::HandlerRejected => write!(f, "Handler rejected the request"),
            RPCError::Disconnected => write!(f, "Gracefully Disconnected"),
        }
    }
}

impl std::error::Error for RPCError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match *self {
            // NOTE: this does have a source
            RPCError::SSZDecodeError(_) => None,
            RPCError::IoError(_) => None,
            RPCError::StreamTimeout => None,
            RPCError::UnsupportedProtocol => None,
            RPCError::IncompleteStream => None,
            RPCError::InvalidData(_) => None,
            RPCError::InternalError(_) => None,
            RPCError::ErrorResponse(_, _) => None,
            RPCError::NegotiationTimeout => None,
            RPCError::HandlerRejected => None,
            RPCError::Disconnected => None,
        }
    }
}

impl<E: EthSpec> std::fmt::Display for RequestType<E> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RequestType::Status(status) => write!(f, "Status Message: {}", status),
            RequestType::Goodbye(reason) => write!(f, "Goodbye: {}", reason),
            RequestType::BlocksByRange(req) => write!(f, "Blocks by range: {}", req),
            RequestType::BlocksByRoot(req) => write!(f, "Blocks by root: {:?}", req),
            RequestType::BlocksByHead(req) => write!(f, "Blocks by head: {}", req),
            RequestType::PayloadEnvelopesByRange(req) => {
                write!(f, "Payload envelopes by range: {:?}", req)
            }
            RequestType::PayloadEnvelopesByRoot(req) => {
                write!(f, "Payload envelopes by root: {:?}", req)
            }
            RequestType::BlobsByRange(req) => write!(f, "Blobs by range: {:?}", req),
            RequestType::BlobsByRoot(req) => write!(f, "Blobs by root: {:?}", req),
            RequestType::DataColumnsByRoot(req) => write!(f, "Data columns by root: {:?}", req),
            RequestType::DataColumnsByRange(req) => {
                write!(f, "Data columns by range: {:?}", req)
            }
            RequestType::Ping(ping) => write!(f, "Ping: {}", ping.data),
            RequestType::MetaData(_) => write!(f, "MetaData request"),
            RequestType::LightClientBootstrap(bootstrap) => {
                write!(f, "Light client boostrap: {}", bootstrap.root)
            }
            RequestType::LightClientOptimisticUpdate => {
                write!(f, "Light client optimistic update request")
            }
            RequestType::LightClientFinalityUpdate => {
                write!(f, "Light client finality update request")
            }
            RequestType::LightClientUpdatesByRange(_) => {
                write!(f, "Light client updates by range request")
            }
        }
    }
}

impl RPCError {
    /// Get a `str` representation of the error.
    /// Used for metrics.
    pub fn as_static_str(&self) -> &'static str {
        match self {
            RPCError::ErrorResponse(code, ..) => code.into(),
            e => e.into(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use libp2p::core::UpgradeInfo;
    use std::collections::HashSet;
    use strum::IntoEnumIterator;
    use types::{Hash256, Slot};

    type E = MainnetEthSpec;

    /// Whether this protocol should appear in `currently_supported()` for the given context.
    ///
    /// Uses an exhaustive match so that adding a new `SupportedProtocol` variant
    /// causes a compile error until this function is updated.
    fn expected_in_currently_supported(
        protocol: SupportedProtocol,
        fork_context: &ForkContext,
    ) -> bool {
        use SupportedProtocol::*;
        match protocol {
            StatusV1 | StatusV2 | GoodbyeV1 | PingV1 | BlocksByRangeV1 | BlocksByRangeV2
            | BlocksByRootV1 | BlocksByRootV2 | MetaDataV1 | MetaDataV2 => true,

            BlobsByRangeV1 | BlobsByRootV1 => fork_context.fork_exists(ForkName::Deneb),

            DataColumnsByRootV1 | DataColumnsByRangeV1 | MetaDataV3 => {
                fork_context.spec.is_peer_das_scheduled()
            }

            PayloadEnvelopesByRangeV1 | PayloadEnvelopesByRootV1 => {
                fork_context.fork_exists(ForkName::Gloas)
            }

            BlocksByHeadV1 => fork_context.fork_exists(ForkName::Fulu),

            // Light client protocols are not in currently_supported()
            LightClientBootstrapV1
            | LightClientOptimisticUpdateV1
            | LightClientFinalityUpdateV1
            | LightClientUpdatesByRangeV1 => false,
        }
    }

    /// Whether this protocol should appear in `protocol_info()` when light client server is
    /// enabled.
    ///
    /// Uses an exhaustive match so that adding a new `SupportedProtocol` variant
    /// causes a compile error until this function is updated.
    fn expected_in_protocol_info(protocol: SupportedProtocol, fork_context: &ForkContext) -> bool {
        use SupportedProtocol::*;
        match protocol {
            LightClientBootstrapV1
            | LightClientOptimisticUpdateV1
            | LightClientFinalityUpdateV1
            | LightClientUpdatesByRangeV1 => true,

            _ => expected_in_currently_supported(protocol, fork_context),
        }
    }

    #[test]
    fn all_protocols_registered() {
        for fork in ForkName::list_all() {
            let spec = fork.make_genesis_spec(E::default_spec());
            let fork_context = Arc::new(ForkContext::new::<E>(Slot::new(0), Hash256::ZERO, &spec));

            let currently_supported: HashSet<SupportedProtocol> =
                SupportedProtocol::currently_supported(&fork_context)
                    .into_iter()
                    .map(|pid| pid.versioned_protocol)
                    .collect();

            let rpc_protocol = RPCProtocol::<E> {
                fork_context: fork_context.clone(),
                max_rpc_size: spec.max_payload_size as usize,
                enable_light_client_server: true,
                phantom: PhantomData,
            };
            let protocol_info: HashSet<SupportedProtocol> = rpc_protocol
                .protocol_info()
                .into_iter()
                .map(|pid| pid.versioned_protocol)
                .collect();

            for protocol in SupportedProtocol::iter() {
                assert_eq!(
                    currently_supported.contains(&protocol),
                    expected_in_currently_supported(protocol, &fork_context),
                    "{protocol:?} registration mismatch in currently_supported() at {fork:?}"
                );
                assert_eq!(
                    protocol_info.contains(&protocol),
                    expected_in_protocol_info(protocol, &fork_context),
                    "{protocol:?} registration mismatch in protocol_info() at {fork:?}"
                );
            }
        }
    }
}
