use alloy_primitives::B256;
use alloy_rpc_types_engine::{
    ForkchoiceUpdateError, INVALID_FORK_CHOICE_STATE_ERROR, INVALID_FORK_CHOICE_STATE_ERROR_MSG,
    INVALID_PAYLOAD_ATTRIBUTES_ERROR, INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG, TOO_DEEP_REORG_ERROR,
    TOO_DEEP_REORG_ERROR_MSG,
};
use jsonrpsee_types::error::{
    INTERNAL_ERROR_CODE, INVALID_PARAMS_CODE, INVALID_PARAMS_MSG, SERVER_ERROR_MSG,
};
use reth_engine_primitives::{BeaconForkChoiceUpdateError, BeaconOnNewPayloadError};
use reth_payload_builder_primitives::PayloadBuilderError;
use reth_payload_primitives::{EngineObjectValidationError, VersionSpecificValidationError};
use thiserror::Error;

/// The Engine API result type
pub type EngineApiResult<Ok> = Result<Ok, EngineApiError>;

/// Payload unsupported fork code.
pub const UNSUPPORTED_FORK_CODE: i32 = -38005;
/// Payload unknown error code.
pub const UNKNOWN_PAYLOAD_CODE: i32 = -38001;
/// Request too large error code.
pub const REQUEST_TOO_LARGE_CODE: i32 = -38004;

/// Error message for the request too large error.
const REQUEST_TOO_LARGE_MESSAGE: &str = "Too large request";

/// Error returned by [`EngineApi`][crate::EngineApi]
///
/// Note: This is a high-fidelity error type which can be converted to an RPC error that adheres to
/// the [Engine API spec](https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors).
#[derive(Error, Debug)]
pub enum EngineApiError {
    // **IMPORTANT**: keep error messages in sync with the Engine API spec linked above.
    /// Payload does not exist / is not available.
    #[error("Unknown payload")]
    UnknownPayload,
    /// The payload body request length is too large.
    #[error("requested count too large: {len}")]
    PayloadRequestTooLarge {
        /// The length that was requested.
        len: u64,
    },
    /// Too many requested versioned hashes for blobs request
    #[error("requested blob count too large: {len}")]
    BlobRequestTooLarge {
        /// The length that was requested.
        len: usize,
    },
    /// Thrown if `engine_getPayloadBodiesByRangeV1` contains an invalid range
    #[error("invalid start ({start}) or count ({count})")]
    InvalidBodiesRange {
        /// Start of the range
        start: u64,
        /// Requested number of items
        count: u64,
    },
    /// Terminal block hash mismatch during transition configuration exchange.
    #[error(
        "invalid transition terminal block hash: \
         execution: {execution:?}, consensus: {consensus}"
    )]
    TerminalBlockHash {
        /// Execution terminal block hash. `None` if block number is not found in the database.
        execution: Option<B256>,
        /// Consensus terminal block hash.
        consensus: B256,
    },
    /// An error occurred while processing the fork choice update in the beacon consensus engine.
    #[error(transparent)]
    ForkChoiceUpdate(#[from] BeaconForkChoiceUpdateError),
    /// An error occurred while processing a new payload in the beacon consensus engine.
    #[error(transparent)]
    NewPayload(#[from] BeaconOnNewPayloadError),
    /// Encountered an internal error.
    #[error(transparent)]
    Internal(#[from] Box<dyn core::error::Error + Send + Sync>),
    /// Fetching the payload failed
    #[error(transparent)]
    GetPayloadError(#[from] PayloadBuilderError),
    /// The payload or attributes are known to be malformed before processing.
    #[error(transparent)]
    EngineObjectValidationError(#[from] EngineObjectValidationError),
    /// Requests hash provided, but can't be accepted by the API.
    #[error("requests hash cannot be accepted by the API without `--engine.accept-execution-requests-hash` flag")]
    UnexpectedRequestsHash,
    /// Any other rpc error
    #[error("{0}")]
    Other(jsonrpsee_types::ErrorObject<'static>),
}

impl EngineApiError {
    /// Crates a new [`EngineApiError::Other`] variant.
    pub const fn other(err: jsonrpsee_types::ErrorObject<'static>) -> Self {
        Self::Other(err)
    }
}

/// Helper type to represent the `error` field in the error response:
/// <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
#[derive(serde::Serialize)]
struct ErrorData {
    err: String,
}

impl ErrorData {
    #[inline]
    fn new(err: impl std::fmt::Display) -> Self {
        Self { err: err.to_string() }
    }
}

impl From<EngineApiError> for jsonrpsee_types::error::ErrorObject<'static> {
    fn from(error: EngineApiError) -> Self {
        match error {
            // Per the Shanghai Engine API spec, FCU V2 must return -38003 when the wrong
            // PayloadAttributes version is used.
            // Spec: https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md
            // Change: https://github.com/ethereum/execution-apis/pull/761
            EngineApiError::EngineObjectValidationError(
                EngineObjectValidationError::PayloadAttributes(
                    VersionSpecificValidationError::WithdrawalsNotSupportedInV1 |
                    VersionSpecificValidationError::NoWithdrawalsPostShanghai |
                    VersionSpecificValidationError::HasWithdrawalsPreShanghai |
                    VersionSpecificValidationError::BlockAccessListNotSupported |
                    VersionSpecificValidationError::HasBlockAccessListPreAmsterdam |
                    VersionSpecificValidationError::NoBlockAccessListPostAmsterdam |
                    VersionSpecificValidationError::HasSlotNumberPreAmsterdam |
                    VersionSpecificValidationError::NoSlotNumberPostAmsterdam |
                    VersionSpecificValidationError::SlotNumberNotSupported,
                ),
            ) |
            EngineApiError::UnexpectedRequestsHash => {
                // Note: the data field is not required by the spec, but is also included by other
                // clients
                jsonrpsee_types::error::ErrorObject::owned(
                    INVALID_PAYLOAD_ATTRIBUTES_ERROR,
                    INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
                    Some(ErrorData::new(error)),
                )
            }
            EngineApiError::InvalidBodiesRange { .. } |
            EngineApiError::EngineObjectValidationError(
                EngineObjectValidationError::Payload(_) |
                EngineObjectValidationError::InvalidParams(_),
            ) => jsonrpsee_types::error::ErrorObject::owned(
                INVALID_PARAMS_CODE,
                INVALID_PARAMS_MSG,
                Some(ErrorData::new(error)),
            ),
            EngineApiError::UnknownPayload => jsonrpsee_types::error::ErrorObject::owned(
                UNKNOWN_PAYLOAD_CODE,
                error.to_string(),
                None::<()>,
            ),
            EngineApiError::PayloadRequestTooLarge { .. } |
            EngineApiError::BlobRequestTooLarge { .. } => {
                jsonrpsee_types::error::ErrorObject::owned(
                    REQUEST_TOO_LARGE_CODE,
                    REQUEST_TOO_LARGE_MESSAGE,
                    Some(ErrorData::new(error)),
                )
            }
            EngineApiError::EngineObjectValidationError(
                EngineObjectValidationError::PayloadAttributes(
                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3 |
                    VersionSpecificValidationError::NoParentBeaconBlockRootPostCancun,
                ),
            ) => jsonrpsee_types::error::ErrorObject::owned(
                INVALID_PAYLOAD_ATTRIBUTES_ERROR,
                INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
                Some(ErrorData::new(error)),
            ),
            EngineApiError::EngineObjectValidationError(
                EngineObjectValidationError::UnsupportedFork,
            ) => jsonrpsee_types::error::ErrorObject::owned(
                UNSUPPORTED_FORK_CODE,
                error.to_string(),
                None::<()>,
            ),
            // Error responses from the consensus engine
            EngineApiError::ForkChoiceUpdate(ref err) => match err {
                BeaconForkChoiceUpdateError::ForkchoiceUpdateError(err) => match err {
                    ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes => {
                        jsonrpsee_types::error::ErrorObject::owned(
                            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
                            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
                            None::<()>,
                        )
                    }
                    ForkchoiceUpdateError::InvalidState |
                    ForkchoiceUpdateError::UnknownFinalBlock => {
                        jsonrpsee_types::error::ErrorObject::owned(
                            INVALID_FORK_CHOICE_STATE_ERROR,
                            INVALID_FORK_CHOICE_STATE_ERROR_MSG,
                            None::<()>,
                        )
                    }
                    ForkchoiceUpdateError::TooDeepReorg => {
                        jsonrpsee_types::error::ErrorObject::owned(
                            TOO_DEEP_REORG_ERROR,
                            TOO_DEEP_REORG_ERROR_MSG,
                            None::<()>,
                        )
                    }
                    // Map future alloy forkchoice errors as internal until handled.
                    #[allow(unreachable_patterns, clippy::needless_return)]
                    _ => {
                        return jsonrpsee_types::error::ErrorObject::owned(
                            INTERNAL_ERROR_CODE,
                            SERVER_ERROR_MSG,
                            Some(ErrorData::new(error)),
                        );
                    }
                },
                BeaconForkChoiceUpdateError::EngineUnavailable |
                BeaconForkChoiceUpdateError::Internal(_) => {
                    jsonrpsee_types::error::ErrorObject::owned(
                        INTERNAL_ERROR_CODE,
                        SERVER_ERROR_MSG,
                        Some(ErrorData::new(error)),
                    )
                }
            },
            // Any other server error
            EngineApiError::TerminalBlockHash { .. } |
            EngineApiError::NewPayload(_) |
            EngineApiError::Internal(_) |
            EngineApiError::GetPayloadError(_) => jsonrpsee_types::error::ErrorObject::owned(
                INTERNAL_ERROR_CODE,
                SERVER_ERROR_MSG,
                Some(ErrorData::new(error)),
            ),
            EngineApiError::Other(err) => err,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use alloy_rpc_types_engine::ForkchoiceUpdateError;
    #[track_caller]
    fn ensure_engine_rpc_error(
        code: i32,
        message: &str,
        err: impl Into<jsonrpsee_types::error::ErrorObject<'static>>,
    ) {
        let err = err.into();
        assert_eq!(err.code(), code);
        assert_eq!(err.message(), message);
    }

    // Tests that engine errors are formatted correctly according to the engine API spec
    // <https://github.com/ethereum/execution-apis/blob/main/src/engine/common.md#errors>
    #[test]
    fn engine_error_rpc_error_test() {
        ensure_engine_rpc_error(
            UNSUPPORTED_FORK_CODE,
            "Unsupported fork",
            EngineApiError::EngineObjectValidationError(
                EngineObjectValidationError::UnsupportedFork,
            ),
        );

        ensure_engine_rpc_error(
            REQUEST_TOO_LARGE_CODE,
            "Too large request",
            EngineApiError::PayloadRequestTooLarge { len: 0 },
        );

        ensure_engine_rpc_error(
            -38002,
            "Invalid forkchoice state",
            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
                ForkchoiceUpdateError::InvalidState,
            )),
        );

        // ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes is for semantic validation
        // errors that occur AFTER the structure check passes, so it returns -38003
        ensure_engine_rpc_error(
            -38003,
            "Invalid payload attributes",
            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
                ForkchoiceUpdateError::UpdatedInvalidPayloadAttributes,
            )),
        );

        ensure_engine_rpc_error(
            -38006,
            "Too deep reorg",
            EngineApiError::ForkChoiceUpdate(BeaconForkChoiceUpdateError::ForkchoiceUpdateError(
                ForkchoiceUpdateError::TooDeepReorg,
            )),
        );

        ensure_engine_rpc_error(
            UNKNOWN_PAYLOAD_CODE,
            "Unknown payload",
            EngineApiError::UnknownPayload,
        );

        // Per the Shanghai Engine API spec, FCU V2 must return -38003 when the wrong
        // PayloadAttributes version is used.
        // Spec: https://github.com/ethereum/execution-apis/blob/main/src/engine/shanghai.md
        // Change: https://github.com/ethereum/execution-apis/pull/761
        ensure_engine_rpc_error(
            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
            EngineApiError::EngineObjectValidationError(
                EngineObjectValidationError::PayloadAttributes(
                    VersionSpecificValidationError::NoWithdrawalsPostShanghai,
                ),
            ),
        );

        ensure_engine_rpc_error(
            INVALID_PARAMS_CODE,
            INVALID_PARAMS_MSG,
            EngineApiError::EngineObjectValidationError(EngineObjectValidationError::Payload(
                VersionSpecificValidationError::NoWithdrawalsPostShanghai,
            )),
        );

        ensure_engine_rpc_error(
            INVALID_PARAMS_CODE,
            INVALID_PARAMS_MSG,
            EngineApiError::EngineObjectValidationError(EngineObjectValidationError::Payload(
                VersionSpecificValidationError::HasWithdrawalsPreShanghai,
            )),
        );

        ensure_engine_rpc_error(
            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
            EngineApiError::EngineObjectValidationError(
                EngineObjectValidationError::PayloadAttributes(
                    VersionSpecificValidationError::HasWithdrawalsPreShanghai,
                ),
            ),
        );

        // Beacon root shape mismatches on PayloadAttributes are reported as -38003.
        ensure_engine_rpc_error(
            INVALID_PAYLOAD_ATTRIBUTES_ERROR,
            INVALID_PAYLOAD_ATTRIBUTES_ERROR_MSG,
            EngineApiError::EngineObjectValidationError(
                EngineObjectValidationError::PayloadAttributes(
                    VersionSpecificValidationError::ParentBeaconBlockRootNotSupportedBeforeV3,
                ),
            ),
        );
    }
}
