use tonic::Code;
use types::error::ErrorCategory;

use crate::proto::google::rpc::{BadRequest, ErrorInfo, RetryInfo};
pub use crate::proto::soma::ErrorReason;

pub type Result<T, E = RpcError> = std::result::Result<T, E>;

/// An error encountered while serving an RPC request.
///
/// General error type used by top-level RPC service methods. The main purpose of this error type
/// is to provide a convenient type for converting between internal errors and a response that
/// needs to be sent to a calling client.
#[derive(Debug)]
pub struct RpcError {
    code: Code,
    message: Option<String>,
    details: Option<Box<ErrorDetails>>,
}

impl RpcError {
    pub fn new<T: Into<String>>(code: Code, message: T) -> Self {
        Self { code, message: Some(message.into()), details: None }
    }

    pub fn not_found() -> Self {
        Self { code: Code::NotFound, message: None, details: None }
    }

    pub fn into_status_proto(self) -> crate::proto::google::rpc::Status {
        crate::proto::google::rpc::Status {
            code: self.code.into(),
            message: self.message.unwrap_or_default(),
            details: self.details.map(ErrorDetails::into_status_details).unwrap_or_default(),
        }
    }
}

impl From<tonic::Status> for RpcError {
    fn from(status: tonic::Status) -> Self {
        use prost::Message;

        let code = status.code();
        let message = status.message();

        // Try to decode the details as a google.rpc.Status proto
        let details = if !status.details().is_empty() {
            crate::proto::google::rpc::Status::decode(status.details())
                .ok()
                .and_then(|status_proto| ErrorDetails::from_status_details(status_proto.details))
                .map(Box::new)
        } else {
            None
        };

        Self {
            code,
            message: if message.is_empty() { None } else { Some(message.to_string()) },
            details,
        }
    }
}

impl From<RpcError> for tonic::Status {
    fn from(value: RpcError) -> Self {
        use prost::Message;

        let code = value.code;
        let status = value.into_status_proto();
        let details = status.encode_to_vec().into();
        let message = status.message;

        tonic::Status::with_details(code, message, details)
    }
}

impl From<types::storage::storage_error::Error> for RpcError {
    fn from(value: types::storage::storage_error::Error) -> Self {
        use types::storage::storage_error::Kind;

        let code = match value.kind() {
            Kind::Missing => Code::NotFound,
            _ => Code::Internal,
        };

        Self { code, message: Some(value.to_string()), details: None }
    }
}

impl From<anyhow::Error> for RpcError {
    fn from(value: anyhow::Error) -> Self {
        Self { code: Code::Internal, message: Some(value.to_string()), details: None }
    }
}

impl From<crate::utils::types_conversions::SdkTypeConversionError> for RpcError {
    fn from(value: crate::utils::types_conversions::SdkTypeConversionError) -> Self {
        Self { code: Code::Internal, message: Some(value.to_string()), details: None }
    }
}

impl From<bcs::Error> for RpcError {
    fn from(value: bcs::Error) -> Self {
        Self { code: Code::Internal, message: Some(value.to_string()), details: None }
    }
}

impl From<types::quorum_driver::QuorumDriverError> for RpcError {
    fn from(error: types::quorum_driver::QuorumDriverError) -> Self {
        use itertools::Itertools;
        use types::quorum_driver::QuorumDriverError::*;

        match error {
            InvalidUserSignature(err) => {
                let message = {
                    let err = err.to_string();
                    format!("Invalid user signature: {err}")
                };

                RpcError::new(Code::InvalidArgument, message)
            }
            QuorumDriverInternalError(err) => RpcError::new(Code::Internal, err.to_string()),
            ObjectsDoubleUsed { conflicting_txes } => {
                let new_map = conflicting_txes
                    .into_iter()
                    .map(|(digest, (pairs, _))| {
                        (digest, pairs.into_iter().map(|(_, obj_ref)| obj_ref).collect())
                    })
                    .collect::<std::collections::BTreeMap<_, Vec<_>>>();

                let message = format!(
                    "Failed to sign transaction by a quorum of validators because of locked objects. Conflicting Transactions:\n{new_map:#?}",
                );

                RpcError::new(Code::FailedPrecondition, message)
            }
            TimeoutBeforeFinality | FailedWithTransientErrorAfterMaximumAttempts { .. } => {
                // TODO add a Retry-After header
                RpcError::new(Code::Unavailable, "timed-out before finality could be reached")
            }
            TimeoutBeforeFinalityWithErrors { last_error, attempts, timeout } => {
                // TODO add a Retry-After header
                RpcError::new(
                    Code::Unavailable,
                    format!(
                        "Transaction timed out before finality could be reached. Attempts: {attempts} & timeout: {timeout:?}. Last error: {last_error}"
                    ),
                )
            }
            NonRecoverableTransactionError { errors } => {
                let new_errors: Vec<String> = errors
                    .into_iter()
                    // sort by total stake, descending, so users see the most prominent one first
                    .sorted_by(|(_, a, _), (_, b, _)| b.cmp(a))
                    .filter_map(
                        |(err, _, _)| {
                            if err.is_retryable().0 { None } else { Some(err.to_string()) }
                        },
                    )
                    .collect();

                assert!(
                    !new_errors.is_empty(),
                    "NonRecoverableTransactionError should have at least one non-retryable error"
                );

                let error_list = new_errors.join(", ");
                let error_msg = format!(
                    "Transaction execution failed due to issues with transaction inputs, please review the errors and try again: {}.",
                    error_list
                );

                RpcError::new(Code::InvalidArgument, error_msg)
            }
            TxAlreadyFinalizedWithDifferentUserSignatures => RpcError::new(
                Code::Aborted,
                "The transaction is already finalized but with different user signatures",
            ),
            // SystemOverload { .. } | SystemOverloadRetryAfter { .. } => {
            //     // TODO add a Retry-After header
            //     RpcError::new(Code::Unavailable, "system is overloaded")
            // }
            TransactionFailed { category, details } => RpcError::new(
                // TODO(fastpath): add a Retry-After header.
                match category {
                    ErrorCategory::Internal => Code::Internal,
                    ErrorCategory::Aborted => Code::Aborted,
                    ErrorCategory::InvalidTransaction => Code::InvalidArgument,
                    ErrorCategory::LockConflict => Code::FailedPrecondition,
                    // ErrorCategory::ValidatorOverloaded => Code::ResourceExhausted,
                    ErrorCategory::Unavailable => Code::Unavailable,
                },
                details,
            ),
            PendingExecutionInTransactionOrchestrator => RpcError::new(
                Code::AlreadyExists,
                "Transaction is already being processed in transaction orchestrator (most likely by quorum driver), wait for results",
            ),
        }
    }
}

impl From<crate::proto::google::rpc::bad_request::FieldViolation> for RpcError {
    fn from(value: crate::proto::google::rpc::bad_request::FieldViolation) -> Self {
        BadRequest::from(value).into()
    }
}

impl From<BadRequest> for RpcError {
    fn from(value: BadRequest) -> Self {
        let message = value.field_violations.first().map(|violation| violation.description.clone());
        let details = ErrorDetails::new().with_bad_request(value);

        RpcError { code: Code::InvalidArgument, message, details: Some(Box::new(details)) }
    }
}

#[derive(Clone, Debug, Default)]
pub struct ErrorDetails {
    error_info: Option<ErrorInfo>,
    bad_request: Option<BadRequest>,
    retry_info: Option<RetryInfo>,
}

impl ErrorDetails {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn error_info(&self) -> Option<&ErrorInfo> {
        self.error_info.as_ref()
    }

    pub fn bad_request(&self) -> Option<&BadRequest> {
        self.bad_request.as_ref()
    }

    pub fn retry_info(&self) -> Option<&RetryInfo> {
        self.retry_info.as_ref()
    }

    pub fn details(&self) -> &[prost_types::Any] {
        &[]
    }

    pub fn with_bad_request(mut self, bad_request: BadRequest) -> Self {
        self.bad_request = Some(bad_request);
        self
    }

    #[allow(clippy::boxed_local)]
    fn into_status_details(self: Box<Self>) -> Vec<prost_types::Any> {
        let mut details = Vec::new();

        if let Some(error_info) = &self.error_info {
            details.push(
                prost_types::Any::from_msg(error_info).expect("Message encoding cannot fail"),
            );
        }

        if let Some(bad_request) = &self.bad_request {
            details.push(
                prost_types::Any::from_msg(bad_request).expect("Message encoding cannot fail"),
            );
        }

        if let Some(retry_info) = &self.retry_info {
            details.push(
                prost_types::Any::from_msg(retry_info).expect("Message encoding cannot fail"),
            );
        }
        details
    }

    pub fn from_status_details(details: Vec<prost_types::Any>) -> Option<Self> {
        use prost::Name;

        if details.is_empty() {
            return None;
        }

        let mut error_details = Self::default();
        let mut has_any = false;

        for any in details {
            if any.type_url == ErrorInfo::type_url() {
                if let Ok(error_info) = any.to_msg() {
                    error_details.error_info = Some(error_info);
                    has_any = true;
                }
            } else if any.type_url == BadRequest::type_url() {
                if let Ok(bad_request) = any.to_msg() {
                    error_details.bad_request = Some(bad_request);
                    has_any = true;
                }
            } else if any.type_url == RetryInfo::type_url()
                && let Ok(retry_info) = any.to_msg()
            {
                error_details.retry_info = Some(retry_info);
                has_any = true;
            }
        }

        has_any.then_some(error_details)
    }
}

#[derive(Debug)]
pub struct ObjectNotFoundError {
    object_id: crate::types::Address,
    version: Option<crate::types::Version>,
}

impl ObjectNotFoundError {
    pub fn new(object_id: crate::types::Address) -> Self {
        Self { object_id, version: None }
    }

    pub fn new_with_version(
        object_id: crate::types::Address,
        version: crate::types::Version,
    ) -> Self {
        Self { object_id, version: Some(version) }
    }
}

impl std::fmt::Display for ObjectNotFoundError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Object {}", self.object_id)?;

        if let Some(version) = self.version {
            write!(f, " with version {version}")?;
        }

        write!(f, " not found")
    }
}

impl std::error::Error for ObjectNotFoundError {}

impl From<ObjectNotFoundError> for crate::api::error::RpcError {
    fn from(value: ObjectNotFoundError) -> Self {
        Self::new(tonic::Code::NotFound, value.to_string())
    }
}

#[derive(Debug)]
pub struct CheckpointNotFoundError {
    sequence_number: Option<u64>,
    digest: Option<crate::types::Digest>,
}

impl CheckpointNotFoundError {
    pub fn sequence_number(sequence_number: u64) -> Self {
        Self { sequence_number: Some(sequence_number), digest: None }
    }

    pub fn digest(digest: crate::types::Digest) -> Self {
        Self { sequence_number: None, digest: Some(digest) }
    }
}

impl std::fmt::Display for CheckpointNotFoundError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "Checkpoint ")?;

        if let Some(s) = self.sequence_number {
            write!(f, "{s} ")?;
        }

        if let Some(d) = &self.digest {
            write!(f, "{d} ")?;
        }

        write!(f, "not found")
    }
}

impl std::error::Error for CheckpointNotFoundError {}

impl From<CheckpointNotFoundError> for crate::api::error::RpcError {
    fn from(value: CheckpointNotFoundError) -> Self {
        Self::new(tonic::Code::NotFound, value.to_string())
    }
}

impl std::fmt::Display for RpcError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self.message {
            Some(msg) => write!(f, "{}: {}", self.code, msg),
            None => write!(f, "{}", self.code),
        }
    }
}

impl std::error::Error for RpcError {}
