use crate::engines::ForkchoiceState;
use crate::http::{
    ENGINE_FORKCHOICE_UPDATED_V1, ENGINE_FORKCHOICE_UPDATED_V2, ENGINE_FORKCHOICE_UPDATED_V3,
    ENGINE_FORKCHOICE_UPDATED_V4, ENGINE_GET_BLOBS_V2, ENGINE_GET_CLIENT_VERSION_V1,
    ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1, ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1,
    ENGINE_GET_PAYLOAD_V1, ENGINE_GET_PAYLOAD_V2, ENGINE_GET_PAYLOAD_V3, ENGINE_GET_PAYLOAD_V4,
    ENGINE_GET_PAYLOAD_V5, ENGINE_GET_PAYLOAD_V6, ENGINE_NEW_PAYLOAD_V1, ENGINE_NEW_PAYLOAD_V2,
    ENGINE_NEW_PAYLOAD_V3, ENGINE_NEW_PAYLOAD_V4, ENGINE_NEW_PAYLOAD_V5,
};
use eth2::types::{
    BlobsBundle, SsePayloadAttributes, SsePayloadAttributesV1, SsePayloadAttributesV2,
    SsePayloadAttributesV3,
};
use http::deposit_methods::RpcError;
pub use json_structures::{JsonWithdrawal, TransitionConfigurationV1};
use pretty_reqwest_error::PrettyReqwestError;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use strum::IntoStaticStr;
use superstruct::superstruct;
pub use types::{
    Address, BeaconBlockRef, ConsolidationRequest, EthSpec, ExecutionBlockHash, ExecutionPayload,
    ExecutionPayloadHeader, ExecutionPayloadRef, ForkName, Hash256, Transactions, Uint256,
    Withdrawal, Withdrawals,
};
use types::{
    ExecutionPayloadBellatrix, ExecutionPayloadCapella, ExecutionPayloadDeneb,
    ExecutionPayloadElectra, ExecutionPayloadFulu, ExecutionPayloadGloas, ExecutionRequests,
    KzgProofs,
};
use types::{GRAFFITI_BYTES_LEN, Graffiti};

pub mod auth;
pub mod http;
pub mod json_structures;
mod new_payload_request;

pub use new_payload_request::{
    NewPayloadRequest, NewPayloadRequestBellatrix, NewPayloadRequestCapella,
    NewPayloadRequestDeneb, NewPayloadRequestElectra, NewPayloadRequestFulu,
    NewPayloadRequestGloas,
};

pub const LATEST_TAG: &str = "latest";

pub type PayloadId = [u8; 8];

#[derive(Debug)]
pub enum Error {
    HttpClient(PrettyReqwestError),
    Auth(auth::Error),
    BadResponse(String),
    RequestFailed(String),
    InvalidExecutePayloadResponse(&'static str),
    JsonRpc(RpcError),
    Json(serde_json::Error),
    ServerMessage { code: i64, message: String },
    Eip155Failure,
    IsSyncing,
    ExecutionBlockNotFound(ExecutionBlockHash),
    ExecutionHeadBlockNotFound,
    ParentHashEqualsBlockHash(ExecutionBlockHash),
    PayloadIdUnavailable,
    SszError(ssz_types::Error),
    DeserializeWithdrawals(ssz_types::Error),
    DeserializeDepositRequests(ssz_types::Error),
    DeserializeWithdrawalRequests(ssz_types::Error),
    BuilderApi(builder_client::Error),
    IncorrectStateVariant,
    RequiredMethodUnsupported(&'static str),
    UnsupportedForkVariant(String),
    InvalidClientVersion(String),
    TooManyConsolidationRequests(usize),
}

impl From<reqwest::Error> for Error {
    fn from(e: reqwest::Error) -> Self {
        if matches!(
            e.status(),
            Some(StatusCode::UNAUTHORIZED) | Some(StatusCode::FORBIDDEN)
        ) {
            Error::Auth(auth::Error::InvalidToken(e.to_string()))
        } else {
            Error::HttpClient(e.into())
        }
    }
}

impl From<serde_json::Error> for Error {
    fn from(e: serde_json::Error) -> Self {
        Error::Json(e)
    }
}

impl From<auth::Error> for Error {
    fn from(e: auth::Error) -> Self {
        Error::Auth(e)
    }
}

impl From<builder_client::Error> for Error {
    fn from(e: builder_client::Error) -> Self {
        Error::BuilderApi(e)
    }
}

impl From<ssz_types::Error> for Error {
    fn from(e: ssz_types::Error) -> Self {
        Error::SszError(e)
    }
}

#[derive(Clone, Copy, Debug, PartialEq, IntoStaticStr)]
#[strum(serialize_all = "snake_case")]
pub enum PayloadStatusV1Status {
    Valid,
    Invalid,
    Syncing,
    Accepted,
    InvalidBlockHash,
}

#[derive(Clone, Debug, PartialEq)]
pub struct PayloadStatusV1 {
    pub status: PayloadStatusV1Status,
    pub latest_valid_hash: Option<ExecutionBlockHash>,
    pub validation_error: Option<String>,
}

#[derive(Clone, Copy, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum BlockByNumberQuery<'a> {
    Tag(&'a str),
}

/// Representation of an exection block with enough detail to determine the terminal PoW block.
///
/// See `get_pow_block_hash_at_total_difficulty`.
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionBlock {
    #[serde(rename = "hash")]
    pub block_hash: ExecutionBlockHash,
    #[serde(rename = "number", with = "serde_utils::u64_hex_be")]
    pub block_number: u64,

    pub parent_hash: ExecutionBlockHash,
    pub total_difficulty: Option<Uint256>,
    #[serde(with = "serde_utils::u64_hex_be")]
    pub timestamp: u64,
}

impl ExecutionBlock {
    pub fn terminal_total_difficulty_reached(&self, terminal_total_difficulty: Uint256) -> bool {
        self.total_difficulty
            .is_none_or(|td| td >= terminal_total_difficulty)
    }
}

#[superstruct(
    variants(V1, V2, V3, V4),
    variant_attributes(derive(Clone, Debug, Eq, Hash, PartialEq),),
    cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"),
    partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant")
)]
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct PayloadAttributes {
    #[superstruct(getter(copy))]
    pub timestamp: u64,
    #[superstruct(getter(copy))]
    pub prev_randao: Hash256,
    #[superstruct(getter(copy))]
    pub suggested_fee_recipient: Address,
    #[superstruct(only(V2, V3, V4))]
    pub withdrawals: Vec<Withdrawal>,
    #[superstruct(only(V3, V4), partial_getter(copy))]
    pub parent_beacon_block_root: Hash256,
    #[superstruct(only(V4), partial_getter(copy))]
    pub slot_number: u64,
    #[superstruct(only(V4), partial_getter(copy))]
    pub target_gas_limit: u64,
}

impl PayloadAttributes {
    pub fn new(
        timestamp: u64,
        prev_randao: Hash256,
        suggested_fee_recipient: Address,
        withdrawals: Option<Vec<Withdrawal>>,
        parent_beacon_block_root: Option<Hash256>,
        slot_number: Option<u64>,
        target_gas_limit: Option<u64>,
    ) -> Self {
        match (
            withdrawals,
            parent_beacon_block_root,
            slot_number,
            target_gas_limit,
        ) {
            (
                Some(withdrawals),
                Some(parent_beacon_block_root),
                Some(slot_number),
                Some(target_gas_limit),
            ) => PayloadAttributes::V4(PayloadAttributesV4 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
                withdrawals,
                parent_beacon_block_root,
                slot_number,
                target_gas_limit,
            }),
            (Some(withdrawals), Some(parent_beacon_block_root), _, _) => {
                PayloadAttributes::V3(PayloadAttributesV3 {
                    timestamp,
                    prev_randao,
                    suggested_fee_recipient,
                    withdrawals,
                    parent_beacon_block_root,
                })
            }
            (Some(withdrawals), None, _, _) => PayloadAttributes::V2(PayloadAttributesV2 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
                withdrawals,
            }),
            (None, _, _, _) => PayloadAttributes::V1(PayloadAttributesV1 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
            }),
        }
    }
}

impl From<PayloadAttributes> for SsePayloadAttributes {
    fn from(pa: PayloadAttributes) -> Self {
        match pa {
            PayloadAttributes::V1(PayloadAttributesV1 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
            }) => Self::V1(SsePayloadAttributesV1 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
            }),
            PayloadAttributes::V2(PayloadAttributesV2 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
                withdrawals,
            }) => Self::V2(SsePayloadAttributesV2 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
                withdrawals,
            }),
            PayloadAttributes::V3(PayloadAttributesV3 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
                withdrawals,
                parent_beacon_block_root,
            }) => Self::V3(SsePayloadAttributesV3 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
                withdrawals,
                parent_beacon_block_root,
            }),
            // V4 maps to V3 for SSE (slot_number/target_gas_limit are not part of the SSE spec)
            PayloadAttributes::V4(PayloadAttributesV4 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
                withdrawals,
                parent_beacon_block_root,
                slot_number: _,
                target_gas_limit: _,
            }) => Self::V3(SsePayloadAttributesV3 {
                timestamp,
                prev_randao,
                suggested_fee_recipient,
                withdrawals,
                parent_beacon_block_root,
            }),
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct ForkchoiceUpdatedResponse {
    pub payload_status: PayloadStatusV1,
    pub payload_id: Option<PayloadId>,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ProposeBlindedBlockResponseStatus {
    Valid,
    Invalid,
    Syncing,
}

#[derive(Clone, Debug, PartialEq)]
pub struct ProposeBlindedBlockResponse {
    pub status: ProposeBlindedBlockResponseStatus,
    pub latest_valid_hash: Option<Hash256>,
    pub validation_error: Option<String>,
}

#[superstruct(
    variants(Bellatrix, Capella, Deneb, Electra, Fulu, Gloas),
    variant_attributes(derive(Clone, Debug, PartialEq),),
    map_into(ExecutionPayload),
    map_ref_into(ExecutionPayloadRef),
    cast_error(ty = "Error", expr = "Error::IncorrectStateVariant"),
    partial_getter_error(ty = "Error", expr = "Error::IncorrectStateVariant")
)]
#[derive(Clone, Debug, PartialEq)]
pub struct GetPayloadResponse<E: EthSpec> {
    #[superstruct(
        only(Bellatrix),
        partial_getter(rename = "execution_payload_bellatrix")
    )]
    pub execution_payload: ExecutionPayloadBellatrix<E>,
    #[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))]
    pub execution_payload: ExecutionPayloadCapella<E>,
    #[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))]
    pub execution_payload: ExecutionPayloadDeneb<E>,
    #[superstruct(only(Electra), partial_getter(rename = "execution_payload_electra"))]
    pub execution_payload: ExecutionPayloadElectra<E>,
    #[superstruct(only(Fulu), partial_getter(rename = "execution_payload_fulu"))]
    pub execution_payload: ExecutionPayloadFulu<E>,
    #[superstruct(only(Gloas), partial_getter(rename = "execution_payload_gloas"))]
    pub execution_payload: ExecutionPayloadGloas<E>,
    pub block_value: Uint256,
    #[superstruct(only(Deneb, Electra, Fulu, Gloas))]
    pub blobs_bundle: BlobsBundle<E>,
    #[superstruct(only(Deneb, Electra, Fulu, Gloas), partial_getter(copy))]
    pub should_override_builder: bool,
    #[superstruct(only(Electra, Fulu, Gloas))]
    pub requests: ExecutionRequests<E>,
}

impl<E: EthSpec> GetPayloadResponse<E> {
    pub fn fee_recipient(&self) -> Address {
        ExecutionPayloadRef::from(self.to_ref()).fee_recipient()
    }

    pub fn block_hash(&self) -> ExecutionBlockHash {
        ExecutionPayloadRef::from(self.to_ref()).block_hash()
    }

    pub fn block_number(&self) -> u64 {
        ExecutionPayloadRef::from(self.to_ref()).block_number()
    }
}

impl<'a, E: EthSpec> From<GetPayloadResponseRef<'a, E>> for ExecutionPayloadRef<'a, E> {
    fn from(response: GetPayloadResponseRef<'a, E>) -> Self {
        map_get_payload_response_ref_into_execution_payload_ref!(&'a _, response, |inner, cons| {
            cons(&inner.execution_payload)
        })
    }
}

impl<E: EthSpec> From<GetPayloadResponse<E>> for ExecutionPayload<E> {
    fn from(response: GetPayloadResponse<E>) -> Self {
        map_get_payload_response_into_execution_payload!(response, |inner, cons| {
            cons(inner.execution_payload)
        })
    }
}

impl<E: EthSpec> From<GetPayloadResponse<E>>
    for (
        ExecutionPayload<E>,
        Uint256,
        Option<BlobsBundle<E>>,
        Option<ExecutionRequests<E>>,
    )
{
    fn from(response: GetPayloadResponse<E>) -> Self {
        match response {
            GetPayloadResponse::Bellatrix(inner) => (
                ExecutionPayload::Bellatrix(inner.execution_payload),
                inner.block_value,
                None,
                None,
            ),
            GetPayloadResponse::Capella(inner) => (
                ExecutionPayload::Capella(inner.execution_payload),
                inner.block_value,
                None,
                None,
            ),
            GetPayloadResponse::Deneb(inner) => (
                ExecutionPayload::Deneb(inner.execution_payload),
                inner.block_value,
                Some(inner.blobs_bundle),
                None,
            ),
            GetPayloadResponse::Electra(inner) => (
                ExecutionPayload::Electra(inner.execution_payload),
                inner.block_value,
                Some(inner.blobs_bundle),
                Some(inner.requests),
            ),
            GetPayloadResponse::Fulu(inner) => (
                ExecutionPayload::Fulu(inner.execution_payload),
                inner.block_value,
                Some(inner.blobs_bundle),
                Some(inner.requests),
            ),
            GetPayloadResponse::Gloas(inner) => (
                ExecutionPayload::Gloas(inner.execution_payload),
                inner.block_value,
                Some(inner.blobs_bundle),
                Some(inner.requests),
            ),
        }
    }
}

pub enum GetPayloadResponseType<E: EthSpec> {
    Full(GetPayloadResponse<E>),
    Blinded(GetPayloadResponse<E>),
}

impl<E: EthSpec> GetPayloadResponse<E> {
    pub fn execution_payload_ref(&self) -> ExecutionPayloadRef<'_, E> {
        self.to_ref().into()
    }
}

#[derive(Clone, Debug)]
pub struct ExecutionPayloadBodyV1<E: EthSpec> {
    pub transactions: Transactions<E>,
    pub withdrawals: Option<Withdrawals<E>>,
}

impl<E: EthSpec> ExecutionPayloadBodyV1<E> {
    pub fn to_payload(
        self,
        header: ExecutionPayloadHeader<E>,
    ) -> Result<ExecutionPayload<E>, String> {
        match header {
            ExecutionPayloadHeader::Bellatrix(header) => {
                if self.withdrawals.is_some() {
                    return Err(format!(
                        "block {} is bellatrix but payload body has withdrawals",
                        header.block_hash
                    ));
                }
                Ok(ExecutionPayload::Bellatrix(ExecutionPayloadBellatrix {
                    parent_hash: header.parent_hash,
                    fee_recipient: header.fee_recipient,
                    state_root: header.state_root,
                    receipts_root: header.receipts_root,
                    logs_bloom: header.logs_bloom,
                    prev_randao: header.prev_randao,
                    block_number: header.block_number,
                    gas_limit: header.gas_limit,
                    gas_used: header.gas_used,
                    timestamp: header.timestamp,
                    extra_data: header.extra_data,
                    base_fee_per_gas: header.base_fee_per_gas,
                    block_hash: header.block_hash,
                    transactions: self.transactions,
                }))
            }
            ExecutionPayloadHeader::Capella(header) => {
                if let Some(withdrawals) = self.withdrawals {
                    Ok(ExecutionPayload::Capella(ExecutionPayloadCapella {
                        parent_hash: header.parent_hash,
                        fee_recipient: header.fee_recipient,
                        state_root: header.state_root,
                        receipts_root: header.receipts_root,
                        logs_bloom: header.logs_bloom,
                        prev_randao: header.prev_randao,
                        block_number: header.block_number,
                        gas_limit: header.gas_limit,
                        gas_used: header.gas_used,
                        timestamp: header.timestamp,
                        extra_data: header.extra_data,
                        base_fee_per_gas: header.base_fee_per_gas,
                        block_hash: header.block_hash,
                        transactions: self.transactions,
                        withdrawals,
                    }))
                } else {
                    Err(format!(
                        "block {} is capella but payload body doesn't have withdrawals",
                        header.block_hash
                    ))
                }
            }
            ExecutionPayloadHeader::Deneb(header) => {
                if let Some(withdrawals) = self.withdrawals {
                    Ok(ExecutionPayload::Deneb(ExecutionPayloadDeneb {
                        parent_hash: header.parent_hash,
                        fee_recipient: header.fee_recipient,
                        state_root: header.state_root,
                        receipts_root: header.receipts_root,
                        logs_bloom: header.logs_bloom,
                        prev_randao: header.prev_randao,
                        block_number: header.block_number,
                        gas_limit: header.gas_limit,
                        gas_used: header.gas_used,
                        timestamp: header.timestamp,
                        extra_data: header.extra_data,
                        base_fee_per_gas: header.base_fee_per_gas,
                        block_hash: header.block_hash,
                        transactions: self.transactions,
                        withdrawals,
                        blob_gas_used: header.blob_gas_used,
                        excess_blob_gas: header.excess_blob_gas,
                    }))
                } else {
                    Err(format!(
                        "block {} is post capella but payload body doesn't have withdrawals",
                        header.block_hash
                    ))
                }
            }
            ExecutionPayloadHeader::Electra(header) => {
                if let Some(withdrawals) = self.withdrawals {
                    Ok(ExecutionPayload::Electra(ExecutionPayloadElectra {
                        parent_hash: header.parent_hash,
                        fee_recipient: header.fee_recipient,
                        state_root: header.state_root,
                        receipts_root: header.receipts_root,
                        logs_bloom: header.logs_bloom,
                        prev_randao: header.prev_randao,
                        block_number: header.block_number,
                        gas_limit: header.gas_limit,
                        gas_used: header.gas_used,
                        timestamp: header.timestamp,
                        extra_data: header.extra_data,
                        base_fee_per_gas: header.base_fee_per_gas,
                        block_hash: header.block_hash,
                        transactions: self.transactions,
                        withdrawals,
                        blob_gas_used: header.blob_gas_used,
                        excess_blob_gas: header.excess_blob_gas,
                    }))
                } else {
                    Err(format!(
                        "block {} is post capella but payload body doesn't have withdrawals",
                        header.block_hash
                    ))
                }
            }
            ExecutionPayloadHeader::Fulu(header) => {
                if let Some(withdrawals) = self.withdrawals {
                    Ok(ExecutionPayload::Fulu(ExecutionPayloadFulu {
                        parent_hash: header.parent_hash,
                        fee_recipient: header.fee_recipient,
                        state_root: header.state_root,
                        receipts_root: header.receipts_root,
                        logs_bloom: header.logs_bloom,
                        prev_randao: header.prev_randao,
                        block_number: header.block_number,
                        gas_limit: header.gas_limit,
                        gas_used: header.gas_used,
                        timestamp: header.timestamp,
                        extra_data: header.extra_data,
                        base_fee_per_gas: header.base_fee_per_gas,
                        block_hash: header.block_hash,
                        transactions: self.transactions,
                        withdrawals,
                        blob_gas_used: header.blob_gas_used,
                        excess_blob_gas: header.excess_blob_gas,
                    }))
                } else {
                    Err(format!(
                        "block {} is post capella but payload body doesn't have withdrawals",
                        header.block_hash
                    ))
                }
            }
        }
    }
}

#[derive(Clone, Copy, Debug)]
pub struct EngineCapabilities {
    pub new_payload_v1: bool,
    pub new_payload_v2: bool,
    pub new_payload_v3: bool,
    pub new_payload_v4: bool,
    pub new_payload_v5: bool,
    pub forkchoice_updated_v1: bool,
    pub forkchoice_updated_v2: bool,
    pub forkchoice_updated_v3: bool,
    pub forkchoice_updated_v4: bool,
    pub get_payload_bodies_by_hash_v1: bool,
    pub get_payload_bodies_by_range_v1: bool,
    pub get_payload_v1: bool,
    pub get_payload_v2: bool,
    pub get_payload_v3: bool,
    pub get_payload_v4: bool,
    pub get_payload_v5: bool,
    pub get_payload_v6: bool,
    pub get_client_version_v1: bool,
    pub get_blobs_v2: bool,
    pub get_blobs_v3: bool,
}

impl EngineCapabilities {
    pub fn to_response(&self) -> Vec<&str> {
        let mut response = Vec::new();
        if self.new_payload_v1 {
            response.push(ENGINE_NEW_PAYLOAD_V1);
        }
        if self.new_payload_v2 {
            response.push(ENGINE_NEW_PAYLOAD_V2);
        }
        if self.new_payload_v3 {
            response.push(ENGINE_NEW_PAYLOAD_V3);
        }
        if self.new_payload_v4 {
            response.push(ENGINE_NEW_PAYLOAD_V4);
        }
        if self.new_payload_v5 {
            response.push(ENGINE_NEW_PAYLOAD_V5);
        }
        if self.forkchoice_updated_v1 {
            response.push(ENGINE_FORKCHOICE_UPDATED_V1);
        }
        if self.forkchoice_updated_v2 {
            response.push(ENGINE_FORKCHOICE_UPDATED_V2);
        }
        if self.forkchoice_updated_v3 {
            response.push(ENGINE_FORKCHOICE_UPDATED_V3);
        }
        if self.forkchoice_updated_v4 {
            response.push(ENGINE_FORKCHOICE_UPDATED_V4);
        }
        if self.get_payload_bodies_by_hash_v1 {
            response.push(ENGINE_GET_PAYLOAD_BODIES_BY_HASH_V1);
        }
        if self.get_payload_bodies_by_range_v1 {
            response.push(ENGINE_GET_PAYLOAD_BODIES_BY_RANGE_V1);
        }
        if self.get_payload_v1 {
            response.push(ENGINE_GET_PAYLOAD_V1);
        }
        if self.get_payload_v2 {
            response.push(ENGINE_GET_PAYLOAD_V2);
        }
        if self.get_payload_v3 {
            response.push(ENGINE_GET_PAYLOAD_V3);
        }
        if self.get_payload_v4 {
            response.push(ENGINE_GET_PAYLOAD_V4);
        }
        if self.get_payload_v5 {
            response.push(ENGINE_GET_PAYLOAD_V5);
        }
        if self.get_payload_v6 {
            response.push(ENGINE_GET_PAYLOAD_V6);
        }
        if self.get_client_version_v1 {
            response.push(ENGINE_GET_CLIENT_VERSION_V1);
        }
        if self.get_blobs_v2 {
            response.push(ENGINE_GET_BLOBS_V2);
        }

        response
    }
}

#[derive(Clone, Debug, PartialEq)]
pub enum ClientCode {
    Besu,
    EtherumJS,
    Erigon,
    GoEthereum,
    Grandine,
    Lighthouse,
    Lodestar,
    Nethermind,
    Nimbus,
    TrinExecution,
    Teku,
    Prysm,
    Reth,
    Unknown(String),
}

impl std::fmt::Display for ClientCode {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            ClientCode::Besu => "BU",
            ClientCode::EtherumJS => "EJ",
            ClientCode::Erigon => "EG",
            ClientCode::GoEthereum => "GE",
            ClientCode::Grandine => "GR",
            ClientCode::Lighthouse => "LH",
            ClientCode::Lodestar => "LS",
            ClientCode::Nethermind => "NM",
            ClientCode::Nimbus => "NB",
            ClientCode::TrinExecution => "TE",
            ClientCode::Teku => "TK",
            ClientCode::Prysm => "PM",
            ClientCode::Reth => "RH",
            ClientCode::Unknown(code) => code,
        };
        write!(f, "{}", s)
    }
}

impl TryFrom<String> for ClientCode {
    type Error = String;

    fn try_from(code: String) -> Result<Self, Self::Error> {
        match code.as_str() {
            "BU" => Ok(Self::Besu),
            "EJ" => Ok(Self::EtherumJS),
            "EG" => Ok(Self::Erigon),
            "GE" => Ok(Self::GoEthereum),
            "GR" => Ok(Self::Grandine),
            "LH" => Ok(Self::Lighthouse),
            "LS" => Ok(Self::Lodestar),
            "NM" => Ok(Self::Nethermind),
            "NB" => Ok(Self::Nimbus),
            "TE" => Ok(Self::TrinExecution),
            "TK" => Ok(Self::Teku),
            "PM" => Ok(Self::Prysm),
            "RH" => Ok(Self::Reth),
            string => {
                if string.len() == 2 {
                    Ok(Self::Unknown(code))
                } else {
                    Err(format!("Invalid client code: {}", code))
                }
            }
        }
    }
}

#[derive(Clone, Debug)]
pub struct CommitPrefix(pub String);

impl TryFrom<String> for CommitPrefix {
    type Error = String;

    fn try_from(value: String) -> Result<Self, Self::Error> {
        // Check if the input starts with '0x' and strip it if it does
        let commit_prefix = value.strip_prefix("0x").unwrap_or(&value);

        // Ensure length is exactly 8 characters after '0x' removal
        if commit_prefix.len() != 8 {
            return Err(
                "Input must be exactly 8 characters long (excluding any '0x' prefix)".to_string(),
            );
        }

        // Ensure all characters are valid hex digits
        if commit_prefix.chars().all(|c| c.is_ascii_hexdigit()) {
            Ok(CommitPrefix(commit_prefix.to_lowercase()))
        } else {
            Err("Input must contain only hexadecimal characters".to_string())
        }
    }
}

impl std::fmt::Display for CommitPrefix {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[derive(Clone, Debug)]
pub struct ClientVersionV1 {
    pub code: ClientCode,
    pub name: String,
    pub version: String,
    pub commit: CommitPrefix,
}

impl ClientVersionV1 {
    pub fn calculate_graffiti(
        &self,
        lighthouse_commit_prefix: CommitPrefix,
        validator_graffiti: Option<Graffiti>,
    ) -> Graffiti {
        let append_graffiti_full = format!(
            "{}{}LH{}",
            self.code,
            self.commit
                .0
                .get(..4)
                .unwrap_or(self.commit.0.as_str())
                .to_lowercase(),
            lighthouse_commit_prefix
                .0
                .get(..4)
                .unwrap_or("0000")
                .to_lowercase(),
        );

        // Implement the special case here:
        // https://hackmd.io/@wmoBhF17RAOH2NZ5bNXJVg/BJX2c9gja#SPECIAL-CASE-the-flexible-standard
        let append_graffiti_one_byte = format!(
            "{}{}LH{}",
            self.code,
            self.commit
                .0
                .get(..2)
                .unwrap_or(self.commit.0.as_str())
                .to_lowercase(),
            lighthouse_commit_prefix
                .0
                .get(..2)
                .unwrap_or("00")
                .to_lowercase(),
        );

        let append_graffiti_no_commit = format!("{}LH", self.code);
        let append_graffiti_only_el = format!("{}", self.code);

        let graffiti_string = if let Some(graffiti) = validator_graffiti {
            let graffiti_length = graffiti.as_utf8_lossy().len();
            let graffiti_str = graffiti.as_utf8_lossy();

            // 12 characters for append_graffiti_full, plus one character for spacing
            // that leaves user specified graffiti to be 32-12-1 = 19 characters max, i.e., <20
            if graffiti_length < 20 {
                format!("{} {}", graffiti_str, append_graffiti_full)
            // user-specified graffiti is between 20-23 characters
            } else if (20..24).contains(&graffiti_length) {
                format!("{} {}", graffiti_str, append_graffiti_one_byte)
            // user-specified graffiti is between 24-27 characters
            } else if (24..28).contains(&graffiti_length) {
                format!("{} {}", graffiti_str, append_graffiti_no_commit)
            // user-specified graffiti is between 28-29 characters
            } else if (28..30).contains(&graffiti_length) {
                format!("{} {}", graffiti_str, append_graffiti_only_el)
            // if user-specified graffiti is between 30-32 characters, append nothing
            } else {
                return graffiti;
            }
        } else {
            // if no validator_graffiti (user doesn't specify), use the full client version info graffiti
            append_graffiti_full
        };

        let mut graffiti_bytes = [0u8; GRAFFITI_BYTES_LEN];
        let bytes_to_copy = std::cmp::min(graffiti_string.len(), GRAFFITI_BYTES_LEN);
        graffiti_bytes[..bytes_to_copy]
            .copy_from_slice(&graffiti_string.as_bytes()[..bytes_to_copy]);

        Graffiti::from(graffiti_bytes)
    }
}
