//! Utilities for handling errors generated by the `fs` module and mapping them to FUSE errors

use mountpoint_s3_client::error::{GetObjectError, ObjectClientError};
use tracing::Level;

use crate::fs::error_metadata::ErrorMetadata;
use crate::metablock::InodeError;
use crate::prefetch::PrefetchReadError;
use crate::upload::UploadError;

/// Generate an error that includes a conversion to a libc errno for use in replies to FUSE.
///
/// `mountpoint-s3` is an application, so we'd be happy to just use the [anyhow] crate directly,
/// except that we need to be able to convert every error into a C integer for use as an errno to
/// give FUSE the right reply. This macro builds a little wrapper around an [anyhow::Error] that
/// includes an errno. We also want to preserve the source information for errors whenever possible,
/// so we optionally allow providing a `source:` argument that will chain an error together with
/// this new one.
///
/// # Examples
///
/// If you already have an error, provide it as the source so that printed versions of the error
/// include its source. For example:
///
/// ```ignore
/// let err = client.head_object("amzn-s3-demo-bucket", "mykey").await.expect_err("failed");
/// return Err(err!(libc::ENOENT, source:err, "file does not exist"));
/// ```
/// will print "file does not exist: service error: ...".
///
///
/// Otherwise, build a new error with no source:
///
/// ```ignore
/// return Err(err!(libc::EINVAL, "cannot use O_SYNC on file handle {:?}", fh));
/// ```
#[macro_export]
macro_rules! err {
    // Base case -- don't use directly
    ($errno:expr, __source:$source:expr, $level:expr, $metadata:expr, $message:literal, $($args:tt)*) => {
        Error {
            errno: $errno,
            message: format!($message, $($args)*),
            source: $source,
            level: $level,
            metadata: $metadata,
        }
    };
    // Actual cases
    ($errno:expr, source:$source:expr, $level:expr, metadata:$metadata:expr, $message:literal) => {
        err!($errno, __source:Some(::anyhow::Error::new($source)), $level, $metadata, $message, )
    };
    // For the following cases we construct an error with empty metadata
    ($errno:expr, source:$source:expr, $level:expr, $message:literal, $($args:tt)*) => {
        err!($errno, __source:Some(::anyhow::Error::new($source)), $level, Default::default(), $message, $($args)*)
    };
    ($errno:expr, source:$source:expr, $level:expr, $message:literal) => {
        err!($errno, __source:Some(::anyhow::Error::new($source)), $level, Default::default(), $message,)
    };
    ($errno:expr, source:$source:expr, $message:literal, $($args:tt)*) => {
        err!($errno, __source:Some(::anyhow::Error::new($source)), ::tracing::Level::WARN, Default::default(), $message, $($args)*)
    };
    ($errno:expr, source:$source:expr, $message:literal) => {
        err!($errno, __source:Some(::anyhow::Error::new($source)), ::tracing::Level::WARN, Default::default(), $message,)
    };
    ($errno:expr, $message:literal, $($args:tt)*) => {
        err!($errno, __source:None, ::tracing::Level::WARN, Default::default(), $message, $($args)*)
    };
    ($errno:expr, $message:literal) => {
        err!($errno, __source:None, ::tracing::Level::WARN, Default::default(), $message,)
    };
}

/// A dynamic error type returned by the Mountpoint filesystem. See the [err!] macro for more
/// details.
#[derive(Debug, thiserror::Error)]
pub struct Error {
    pub(crate) errno: libc::c_int,
    pub(crate) message: String,
    pub(crate) source: Option<anyhow::Error>,
    pub(crate) level: Level,
    pub(crate) metadata: ErrorMetadata,
}

impl std::fmt::Display for Error {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        if let Some(source) = self.source.as_ref() {
            // {:#} tells anyhow to include the entire chain of sources for the error
            write!(f, "{}: {:#}", self.message, source)
        } else {
            write!(f, "{}", self.message)
        }
    }
}

impl From<InodeError> for Error {
    fn from(err: InodeError) -> Self {
        let errno = err.to_errno();
        let metadata = err.meta().clone();
        Error {
            errno,
            message: String::from("inode error"),
            source: Some(anyhow::anyhow!(err)),
            // We are having WARN as the default level of logging for fuse errors
            level: Level::WARN,
            metadata,
        }
    }
}

impl<E: std::error::Error + Send + Sync + 'static> From<UploadError<E>> for Error {
    fn from(err: UploadError<E>) -> Self {
        let errno = err.to_errno();
        Error {
            errno,
            message: String::from("upload error"),
            source: Some(anyhow::anyhow!(err)),
            // We are having WARN as the default level of logging for fuse errors
            level: Level::WARN,
            metadata: Default::default(), // TODO (vlaad): must be cloned from UploadError
        }
    }
}

impl<E: std::error::Error + Send + Sync + 'static> From<PrefetchReadError<E>> for Error {
    fn from(err: PrefetchReadError<E>) -> Self {
        match err {
            PrefetchReadError::GetRequestFailed {
                source: ObjectClientError::ServiceError(GetObjectError::PreconditionFailed(_)),
                metadata,
            } => err!(libc::ESTALE, __source:None, Level::WARN, (*metadata).clone(), "object was mutated remotely",),
            PrefetchReadError::GetRequestFailed { source, metadata } => {
                err!(libc::EIO, source:source, Level::WARN, metadata:(*metadata).clone(), "get request failed")
            }
            PrefetchReadError::Integrity(e) => err!(libc::EIO, source:e, "integrity error"),
            PrefetchReadError::PartReadFailed(e) => err!(libc::EIO, source:e, "part read failed"),
            PrefetchReadError::GetRequestTerminatedUnexpectedly
            | PrefetchReadError::GetRequestReturnedWrongOffset { .. }
            | PrefetchReadError::BackpressurePreconditionFailed
            | PrefetchReadError::ReadWindowIncrement => {
                err!(libc::EIO, source:err, "get request failed")
            }
        }
    }
}

/// Errors that can be converted to a raw OS error (errno)
pub trait ToErrno {
    fn to_errno(&self) -> libc::c_int;
}

impl ToErrno for Error {
    fn to_errno(&self) -> libc::c_int {
        self.errno
    }
}

impl ToErrno for InodeError {
    fn to_errno(&self) -> libc::c_int {
        match self {
            InodeError::ClientError { .. } => libc::EIO,
            InodeError::FileDoesNotExist(_, _) => libc::ENOENT,
            InodeError::InodeDoesNotExist(_) => libc::ENOENT,
            InodeError::InvalidFileName(_) => libc::EINVAL,
            InodeError::NotADirectory(_) => libc::ENOTDIR,
            InodeError::IsDirectory(_) => libc::EISDIR,
            InodeError::FileAlreadyExists(_) => libc::EEXIST,
            // Not obvious what InodeNotWritable, InodeAlreadyWriting, InodeNotReadableWhileWriting should be.
            // EINVAL or EROFS would also be reasonable -- but we'll treat them like sealed files.
            InodeError::InodeNotWritable(_) => libc::EPERM,
            InodeError::InodeInvalidWriteStatus(_) => libc::EPERM,
            InodeError::InodeAlreadyWriting(_) => libc::EPERM,
            InodeError::InodeNotReadableWhileWriting(_) => libc::EPERM,
            InodeError::InodeNotWritableWhileReading(_) => libc::EPERM,
            InodeError::CannotRemoveRemoteDirectory(_) => libc::EPERM,
            InodeError::DirectoryNotEmpty(_) => libc::ENOTEMPTY,
            InodeError::UnlinkNotPermittedWhileWriting(_) => libc::EPERM,
            InodeError::CorruptedMetadata(_) => libc::EIO,
            InodeError::SetAttrNotPermittedOnRemoteInode(_) => libc::EPERM,
            InodeError::StaleInode { .. } => libc::ESTALE,
            InodeError::CannotRenameDirectory(_) => libc::EPERM,
            InodeError::RenameDestinationExists { .. } => libc::EEXIST,
            InodeError::RenameNotPermittedWhileWriting(_) => libc::EPERM,
            InodeError::RenameNotSupported() => libc::ENOSYS,
            InodeError::NameTooLong(_) => libc::ENAMETOOLONG,
            #[cfg(feature = "manifest")]
            InodeError::ManifestError { .. } => libc::EIO,
            InodeError::OperationNotSupportedOnSyntheticInode { .. } => libc::EIO,
            InodeError::OutOfOrderReadDir { .. } => libc::EBADF,
            InodeError::NoSuchDirHandle { .. } => libc::EINVAL,
            InodeError::FlexibleRetrievalObjectNotAccessible(_) => libc::EACCES,
        }
    }
}

impl<E: std::error::Error> ToErrno for UploadError<E> {
    fn to_errno(&self) -> libc::c_int {
        match self {
            UploadError::PutRequestFailed(_) => libc::EIO,
            UploadError::UploadAlreadyTerminated => libc::EIO,
            UploadError::SseCorruptedError(_) => libc::EIO,
            UploadError::ChecksumComputationFailed(_) => libc::EIO,
            UploadError::HeadObjectFailed(_) => libc::EIO,
            UploadError::OutOfOrderWrite { .. } => libc::EINVAL,
            UploadError::ObjectTooBig { .. } => libc::EFBIG,
        }
    }
}

impl Error {
    pub fn meta(&self) -> &ErrorMetadata {
        &self.metadata
    }
}
