// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use std::{
    collections::HashSet,
    error::Error,
    fmt, io,
    iter::once,
    str,
    sync::{self, Arc},
};

use crate::engine::ActionAvailability;

pub type StratisResult<T> = Result<T, StratisError>;

#[derive(Debug, Clone)]
pub enum StratisError {
    Msg(String),
    Chained(String, Box<StratisError>),
    /// This variant is for rollback operations like wiping block devices
    /// where the operation may continue even if there are failures
    /// as there is no action to be taken in the event of failures. We
    /// just need to report all of the failures to the users.
    BestEffortError(String, Vec<StratisError>),
    RollbackError {
        causal_error: Box<StratisError>,
        rollback_error: Box<StratisError>,
        level: ActionAvailability,
    },
    ActionAvailabilityError {
        error: Box<StratisError>,
        level: ActionAvailability,
    },
    /// This variant should be used for failed roll back that does not
    /// prompt any action in stratisd but needs to be reported to the user.
    NoActionRollbackError {
        causal_error: Box<StratisError>,
        rollback_error: Box<StratisError>,
    },
    ActionDisabled(ActionAvailability),
    OutOfSpaceError(String),
    Io(Arc<io::Error>),
    Nix(nix::Error),
    Uuid(uuid::Error),
    Utf8(str::Utf8Error),
    Serde(Arc<serde_json::error::Error>),
    Decode(data_encoding::DecodeError),
    DM(devicemapper::DmError),
    Crypt(Arc<libcryptsetup_rs::LibcryptErr>),
    Recv(sync::mpsc::RecvError),
    Null(std::ffi::NulError),
    Join(Arc<tokio::task::JoinError>),
    Blkid(Arc<libblkid_rs::BlkidErr>),

    #[cfg(feature = "dbus_enabled")]
    Dbus(zbus::Error),
    #[cfg(feature = "dbus_enabled")]
    DbusType(zbus::zvariant::Error),
    Udev(Arc<libudev::Error>),
}

impl StratisError {
    /// Determine all possible pool action availability states that result from this
    /// error chain.
    fn error_to_all_available_actions(&self) -> HashSet<ActionAvailability> {
        match self {
            StratisError::Chained(_, c) => c.error_to_all_available_actions(),
            StratisError::ActionAvailabilityError { level, error } => once(level)
                .cloned()
                .chain(error.error_to_all_available_actions())
                .collect::<HashSet<_>>(),
            StratisError::BestEffortError(_, errs) => errs
                .iter()
                .flat_map(|e| e.error_to_all_available_actions())
                .collect::<HashSet<_>>(),
            StratisError::RollbackError {
                level,
                causal_error,
                rollback_error,
            } => once(level)
                .cloned()
                .chain(causal_error.error_to_all_available_actions())
                .chain(rollback_error.error_to_all_available_actions())
                .collect::<HashSet<_>>(),
            StratisError::NoActionRollbackError {
                causal_error,
                rollback_error,
            } => {
                let mut states = causal_error.error_to_all_available_actions();
                states.extend(rollback_error.error_to_all_available_actions());
                states
            }
            _ => HashSet::new(),
        }
    }

    /// Determine the most restrictive pool action availability state required from
    /// the set of all available action states.
    pub fn error_to_available_actions(&self) -> Option<ActionAvailability> {
        self.error_to_all_available_actions().into_iter().max()
    }
}

impl fmt::Display for StratisError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            StratisError::Msg(ref s) => write!(f, "{s}"),
            StratisError::Chained(ref s, ref chained) => write!(f, "{s}; {chained}"),
            StratisError::BestEffortError(ref s, ref errs) => {
                if errs.is_empty() {
                    write!(f, "{s}")
                } else {
                    let errs_string = errs
                        .iter()
                        .map(|err| err.to_string())
                        .collect::<Vec<_>>()
                        .join("; ");
                    write!(f, "{s}; {errs_string}")
                }
            }
            StratisError::RollbackError {
                ref causal_error,
                ref rollback_error,
                ref level,
            } => {
                write!(
                    f,
                    "Rollback failed; causal_error: {causal_error}, rollback error: {rollback_error}; putting pool in action availability state {level}"
                )
            }
            StratisError::NoActionRollbackError {
                ref causal_error,
                ref rollback_error,
            } => {
                write!(
                    f,
                    "Rollback failed; causal_error: {causal_error}, rollback error: {rollback_error}"
                )
            }
            StratisError::OutOfSpaceError(ref msg) => {
                write!(f, "Pool is out of space and cannot be extended: {msg}")
            }
            StratisError::ActionDisabled(ref level) => {
                write!(f, "Pool is in state {level:?} where this action cannot be performed until the issue is resolved manually")
            }
            StratisError::ActionAvailabilityError {
                ref error,
                ref level,
            } => {
                write!(
                    f,
                    "Error that set action availability state {level}: {error}"
                )
            }
            StratisError::Io(ref err) => write!(f, "IO error: {err}"),
            StratisError::Nix(ref err) => write!(f, "Nix error: {err}"),
            StratisError::Uuid(ref err) => write!(f, "Uuid error: {err}"),
            StratisError::Utf8(ref err) => write!(f, "Utf8 error: {err}"),
            StratisError::Serde(ref err) => write!(f, "Serde error: {err}"),
            StratisError::Decode(ref err) => write!(f, "Data encoding error: {err}"),
            StratisError::DM(ref err) => write!(f, "DM error: {err}"),
            StratisError::Crypt(ref err) => write!(f, "Cryptsetup error: {err}"),
            StratisError::Recv(ref err) => write!(f, "Synchronization channel error: {err}"),
            StratisError::Null(ref err) => write!(f, "C string conversion error: {err}"),
            StratisError::Join(ref err) => write!(f, "Failed to join thread: {err}"),
            StratisError::Blkid(ref err) => {
                write!(f, "Failed to probe device using blkid: {err}")
            }

            #[cfg(feature = "dbus_enabled")]
            StratisError::Dbus(ref err) => {
                write!(f, "Dbus error: {err}",)
            }
            #[cfg(feature = "dbus_enabled")]
            StratisError::DbusType(ref err) => {
                write!(f, "Dbus type error: {err}",)
            }
            StratisError::Udev(ref err) => write!(f, "Udev error: {err}"),
        }
    }
}

impl Error for StratisError {}

impl From<libblkid_rs::BlkidErr> for StratisError {
    fn from(err: libblkid_rs::BlkidErr) -> StratisError {
        StratisError::Blkid(Arc::new(err))
    }
}

impl From<tokio::task::JoinError> for StratisError {
    fn from(err: tokio::task::JoinError) -> StratisError {
        StratisError::Join(Arc::new(err))
    }
}

impl From<std::ffi::NulError> for StratisError {
    fn from(err: std::ffi::NulError) -> StratisError {
        StratisError::Null(err)
    }
}

impl From<io::Error> for StratisError {
    fn from(err: io::Error) -> StratisError {
        StratisError::Io(Arc::new(err))
    }
}

impl From<nix::Error> for StratisError {
    fn from(err: nix::Error) -> StratisError {
        StratisError::Nix(err)
    }
}

impl From<uuid::Error> for StratisError {
    fn from(err: uuid::Error) -> StratisError {
        StratisError::Uuid(err)
    }
}

impl From<str::Utf8Error> for StratisError {
    fn from(err: str::Utf8Error) -> StratisError {
        StratisError::Utf8(err)
    }
}

impl From<serde_json::error::Error> for StratisError {
    fn from(err: serde_json::error::Error) -> StratisError {
        StratisError::Serde(Arc::new(err))
    }
}

impl From<data_encoding::DecodeError> for StratisError {
    fn from(err: data_encoding::DecodeError) -> StratisError {
        StratisError::Decode(err)
    }
}

impl From<devicemapper::DmError> for StratisError {
    fn from(err: devicemapper::DmError) -> StratisError {
        StratisError::DM(err)
    }
}

impl From<libcryptsetup_rs::LibcryptErr> for StratisError {
    fn from(err: libcryptsetup_rs::LibcryptErr) -> StratisError {
        StratisError::Crypt(Arc::new(err))
    }
}

#[cfg(feature = "dbus_enabled")]
impl From<zbus::Error> for StratisError {
    fn from(err: zbus::Error) -> StratisError {
        StratisError::Dbus(err)
    }
}

#[cfg(feature = "dbus_enabled")]
impl From<zbus::zvariant::Error> for StratisError {
    fn from(err: zbus::zvariant::Error) -> StratisError {
        StratisError::DbusType(err)
    }
}

impl From<libudev::Error> for StratisError {
    fn from(err: libudev::Error) -> StratisError {
        StratisError::Udev(Arc::new(err))
    }
}

impl From<sync::mpsc::RecvError> for StratisError {
    fn from(err: sync::mpsc::RecvError) -> StratisError {
        StratisError::Recv(err)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_error_to_available_actions() {
        assert_eq!(
            StratisError::Msg("Message".to_string()).error_to_available_actions(),
            None
        );
        assert_eq!(
            StratisError::Chained(
                "Message".to_string(),
                Box::new(StratisError::RollbackError {
                    causal_error: Box::new(StratisError::Msg("Cause".to_string())),
                    rollback_error: Box::new(StratisError::Msg("Rollback".to_string())),
                    level: ActionAvailability::NoRequests,
                }),
            )
            .error_to_available_actions(),
            Some(ActionAvailability::NoRequests)
        );
    }
}
