use std::{error::Error, fmt, io, path::PathBuf};

use crate::landlock::{Access, AccessFs, AccessNet, ScopeFlag};

/// Maps to all errors that can be returned by a ruleset action.
#[derive(Debug)]
#[non_exhaustive]
pub enum RulesetError {
    HandleAccesses(HandleAccessesError),
    CreateRuleset(CreateRulesetError),
    AddRules(AddRulesError),
    RestrictSelf(RestrictSelfError),
}

impl Error for RulesetError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            RulesetError::HandleAccesses(err) => Error::source(err),
            RulesetError::CreateRuleset(err) => Error::source(err),
            RulesetError::AddRules(err) => Error::source(err),
            RulesetError::RestrictSelf(err) => Error::source(err),
        }
    }
}

impl fmt::Display for RulesetError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            RulesetError::HandleAccesses(err) => fmt::Display::fmt(err, f),
            RulesetError::CreateRuleset(err) => fmt::Display::fmt(err, f),
            RulesetError::AddRules(err) => fmt::Display::fmt(err, f),
            RulesetError::RestrictSelf(err) => fmt::Display::fmt(err, f),
        }
    }
}

impl std::convert::From<HandleAccessesError> for RulesetError {
    fn from(source: HandleAccessesError) -> Self {
        RulesetError::HandleAccesses(source)
    }
}

impl std::convert::From<CreateRulesetError> for RulesetError {
    fn from(source: CreateRulesetError) -> Self {
        RulesetError::CreateRuleset(source)
    }
}

impl std::convert::From<AddRulesError> for RulesetError {
    fn from(source: AddRulesError) -> Self {
        RulesetError::AddRules(source)
    }
}

impl std::convert::From<RestrictSelfError> for RulesetError {
    fn from(source: RestrictSelfError) -> Self {
        RulesetError::RestrictSelf(source)
    }
}

#[test]
fn ruleset_error_breaking_change() {
    use crate::*;

    // Generics are part of the API and modifying them can lead to a breaking change.
    let _: RulesetError = RulesetError::HandleAccesses(HandleAccessesError::Fs(
        HandleAccessError::Compat(CompatError::Access(AccessError::Empty)),
    ));
}

/// Identifies errors when updating the ruleset's handled access-rights.
#[derive(Debug)]
#[non_exhaustive]
pub enum HandleAccessError<T>
where
    T: Access,
{
    Compat(CompatError<T>),
}

impl<T> Error for HandleAccessError<T>
where
    T: Access,
    CompatError<T>: Error,
    Self: fmt::Debug + fmt::Display,
{
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            HandleAccessError::Compat(err) => Error::source(err),
        }
    }
}

impl<T> fmt::Display for HandleAccessError<T>
where
    T: Access,
    CompatError<T>: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            HandleAccessError::Compat(err) => fmt::Display::fmt(err, f),
        }
    }
}

impl<T> std::convert::From<CompatError<T>> for HandleAccessError<T>
where
    T: Access,
{
    fn from(source: CompatError<T>) -> Self {
        HandleAccessError::Compat(source)
    }
}

#[derive(Debug)]
#[non_exhaustive]
pub enum HandleAccessesError {
    Fs(HandleAccessError<AccessFs>),
    Net(HandleAccessError<AccessNet>),
    Scoped(HandleAccessError<ScopeFlag>),
}

impl Error for HandleAccessesError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            HandleAccessesError::Fs(err) => Error::source(err),
            HandleAccessesError::Net(err) => Error::source(err),
            HandleAccessesError::Scoped(err) => Error::source(err),
        }
    }
}

impl fmt::Display for HandleAccessesError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            HandleAccessesError::Fs(err) => fmt::Display::fmt(err, f),
            HandleAccessesError::Net(err) => fmt::Display::fmt(err, f),
            HandleAccessesError::Scoped(err) => fmt::Display::fmt(err, f),
        }
    }
}

// Generically implement for all the access implementations rather than for the cases listed in
// HandleAccessesError (with #[from]).
impl<A> From<HandleAccessError<A>> for HandleAccessesError
where
    A: Access,
{
    fn from(error: HandleAccessError<A>) -> Self {
        A::into_handle_accesses_error(error)
    }
}

/// Identifies errors when creating a ruleset.
#[derive(Debug)]
#[non_exhaustive]
pub enum CreateRulesetError {
    /// The `landlock_create_ruleset()` system call failed.
    #[non_exhaustive]
    CreateRulesetCall { source: io::Error },
    /// Missing call to [`RulesetAttr::handle_access()`](crate::RulesetAttr::handle_access).
    MissingHandledAccess,
}

impl Error for CreateRulesetError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            CreateRulesetError::CreateRulesetCall { source, .. } => Some(source),
            CreateRulesetError::MissingHandledAccess => None,
        }
    }
}

impl fmt::Display for CreateRulesetError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CreateRulesetError::CreateRulesetCall { source } => {
                write!(f, "failed to create a ruleset: {source}",)
            }
            CreateRulesetError::MissingHandledAccess => {
                write!(f, "missing handled access")
            }
        }
    }
}

/// Identifies errors when adding a rule to a ruleset.
#[derive(Debug)]
#[non_exhaustive]
pub enum AddRuleError<T>
where
    T: Access,
{
    /// The `landlock_add_rule()` system call failed.
    #[non_exhaustive]
    AddRuleCall {
        source: io::Error,
    },
    /// The rule's access-rights are not all handled by the (requested) ruleset access-rights.
    UnhandledAccess {
        access: T,
        incompatible: T,
    },
    Compat(CompatError<T>),
}

impl<T> Error for AddRuleError<T>
where
    T: Access,
    CompatError<T>: Error,
    Self: fmt::Debug + fmt::Display,
{
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AddRuleError::AddRuleCall { source, .. } => Some(source),
            AddRuleError::UnhandledAccess { .. } => None,
            AddRuleError::Compat(err) => Error::source(err),
        }
    }
}

impl<T> fmt::Display for AddRuleError<T>
where
    T: Access,
    T: fmt::Debug,
    CompatError<T>: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AddRuleError::AddRuleCall { source } => {
                write!(f, "failed to add a rule: {source}",)
            }
            AddRuleError::UnhandledAccess {
                access: _,
                incompatible,
            } => write!(
                f,
                "access-rights not handled by the ruleset: {incompatible:?}",
            ),
            AddRuleError::Compat(err) => fmt::Display::fmt(err, f),
        }
    }
}

impl<T> std::convert::From<CompatError<T>> for AddRuleError<T>
where
    T: Access,
{
    fn from(source: CompatError<T>) -> Self {
        AddRuleError::Compat { 0: source }
    }
}

// Generically implement for all the access implementations rather than for the cases listed in
// AddRulesError (with #[from]).
impl<A> From<AddRuleError<A>> for AddRulesError
where
    A: Access,
{
    fn from(error: AddRuleError<A>) -> Self {
        A::into_add_rules_error(error)
    }
}

/// Identifies errors when adding rules to a ruleset thanks to an iterator returning
/// Result<Rule, E> items.
#[derive(Debug)]
#[non_exhaustive]
pub enum AddRulesError {
    Fs(AddRuleError<AccessFs>),
    Net(AddRuleError<AccessNet>),
    Scoped(AddRuleError<ScopeFlag>),
}

impl Error for AddRulesError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AddRulesError::Fs(err) => Error::source(err),
            AddRulesError::Net(err) => Error::source(err),
            AddRulesError::Scoped(err) => Error::source(err),
        }
    }
}

impl fmt::Display for AddRulesError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AddRulesError::Fs(err) => fmt::Display::fmt(err, f),
            AddRulesError::Net(err) => fmt::Display::fmt(err, f),
            AddRulesError::Scoped(err) => fmt::Display::fmt(err, f),
        }
    }
}

#[derive(Debug)]
#[non_exhaustive]
pub enum CompatError<T>
where
    T: Access,
{
    PathBeneath(PathBeneathError),
    Access(AccessError<T>),
}

impl<T> Error for CompatError<T>
where
    T: Access,
    AccessError<T>: Error,
    Self: fmt::Debug + fmt::Display,
{
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            CompatError::PathBeneath(err) => Error::source(err),
            CompatError::Access(err) => Error::source(err),
        }
    }
}

impl<T> fmt::Display for CompatError<T>
where
    T: Access,
    AccessError<T>: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            CompatError::PathBeneath(err) => fmt::Display::fmt(err, f),
            CompatError::Access(err) => fmt::Display::fmt(err, f),
        }
    }
}

impl<T> std::convert::From<PathBeneathError> for CompatError<T>
where
    T: Access,
{
    fn from(source: PathBeneathError) -> Self {
        CompatError::PathBeneath(source)
    }
}

impl<T> std::convert::From<AccessError<T>> for CompatError<T>
where
    T: Access,
{
    fn from(source: AccessError<T>) -> Self {
        CompatError::Access(source)
    }
}

#[derive(Debug)]
#[non_exhaustive]
pub enum PathBeneathError {
    /// To check that access-rights are consistent with a file descriptor, a call to
    /// [`RulesetCreatedAttr::add_rule()`](crate::RulesetCreatedAttr::add_rule)
    /// looks at the file type with an `fstat()` system call.
    #[non_exhaustive]
    StatCall { source: io::Error },
    /// This error is returned by
    /// [`RulesetCreatedAttr::add_rule()`](crate::RulesetCreatedAttr::add_rule)
    /// if the related PathBeneath object is not set to best-effort,
    /// and if its allowed access-rights contain directory-only ones
    /// whereas the file descriptor doesn't point to a directory.
    DirectoryAccess {
        access: AccessFs,
        incompatible: AccessFs,
    },
}

impl Error for PathBeneathError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            PathBeneathError::StatCall { source, .. } => Some(source),
            PathBeneathError::DirectoryAccess { .. } => None,
        }
    }
}

impl fmt::Display for PathBeneathError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            PathBeneathError::StatCall { source } => {
                write!(f, "failed to check file descriptor type: {source}",)
            }
            PathBeneathError::DirectoryAccess {
                access: _,
                incompatible,
            } => write!(
                f,
                "incompatible directory-only access-rights: {incompatible:?}",
            ),
        }
    }
}

#[derive(Debug)]
// Exhaustive enum
pub enum AccessError<T>
where
    T: Access,
{
    /// The access-rights set is empty, which doesn't make sense and would be rejected by the
    /// kernel.
    Empty,
    /// The best-effort approach was (deliberately) disabled and the requested access-rights are
    /// fully incompatible with the running kernel.
    Incompatible { access: T },
    /// The best-effort approach was (deliberately) disabled and the requested access-rights are
    /// partially incompatible with the running kernel.
    PartiallyCompatible { access: T, incompatible: T },
}

impl<T> Error for AccessError<T>
where
    T: Access,
    Self: fmt::Debug + fmt::Display,
{
}

impl<T> fmt::Display for AccessError<T>
where
    T: Access,
    T: fmt::Debug,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AccessError::Empty => write!(f, "empty access-right"),
            AccessError::Incompatible { access } => write!(
                f,
                "fully incompatible access-rights: {access:?}",
                access = access
            ),
            AccessError::PartiallyCompatible {
                access: _,
                incompatible,
            } => write!(
                f,
                "partially incompatible access-rights: {incompatible:?}",
                incompatible = incompatible
            ),
        }
    }
}

#[derive(Debug)]
#[non_exhaustive]
pub enum RestrictSelfError {
    /// The `prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)` system call failed.
    #[non_exhaustive]
    SetNoNewPrivsCall { source: io::Error },
    /// The `landlock_restrict_self() `system call failed.
    #[non_exhaustive]
    RestrictSelfCall { source: io::Error },
}

impl Error for RestrictSelfError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            RestrictSelfError::SetNoNewPrivsCall { source, .. } => Some(source),
            RestrictSelfError::RestrictSelfCall { source, .. } => Some(source),
        }
    }
}

impl fmt::Display for RestrictSelfError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            RestrictSelfError::SetNoNewPrivsCall { source } => {
                write!(f, "failed to set no_new_privs: {source}",)
            }
            RestrictSelfError::RestrictSelfCall { source } => {
                write!(f, "failed to restrict the calling thread: {source}",)
            }
        }
    }
}

#[derive(Debug)]
#[non_exhaustive]
pub enum PathFdError {
    /// The `open()` system call failed.
    #[non_exhaustive]
    OpenCall { source: io::Error, path: PathBuf },
}

impl Error for PathFdError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            PathFdError::OpenCall { source, .. } => Some(source),
        }
    }
}

impl fmt::Display for PathFdError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            PathFdError::OpenCall { source, path } => {
                write!(
                    f,
                    "failed to open \"{path}\": {source}",
                    path = path.display()
                )
            }
        }
    }
}

#[cfg(test)]
#[derive(Debug)]
pub(crate) enum TestRulesetError {
    Ruleset(RulesetError),
    PathFd(PathFdError),
    File(std::io::Error),
}

#[cfg(test)]
impl Error for TestRulesetError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            TestRulesetError::Ruleset(err) => Error::source(err),
            TestRulesetError::PathFd(err) => Error::source(err),
            TestRulesetError::File(err) => Error::source(err),
        }
    }
}

#[cfg(test)]
impl fmt::Display for TestRulesetError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            TestRulesetError::Ruleset(err) => fmt::Display::fmt(err, f),
            TestRulesetError::PathFd(err) => fmt::Display::fmt(err, f),
            TestRulesetError::File(err) => fmt::Display::fmt(err, f),
        }
    }
}

#[cfg(test)]
impl std::convert::From<RulesetError> for TestRulesetError {
    fn from(source: RulesetError) -> Self {
        TestRulesetError::Ruleset(source)
    }
}

#[cfg(test)]
impl std::convert::From<PathFdError> for TestRulesetError {
    fn from(source: PathFdError) -> Self {
        TestRulesetError::PathFd(source)
    }
}

#[cfg(test)]
impl std::convert::From<std::io::Error> for TestRulesetError {
    fn from(source: std::io::Error) -> Self {
        TestRulesetError::File(source)
    }
}
