use std::{io, path::PathBuf};

use crate::landlock::{Access, AccessFs, BitFlags};

/// 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 std::error::Error for RulesetError {}

impl std::fmt::Display for RulesetError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            RulesetError::HandleAccesses(error) => write!(f, "{error}"),
            RulesetError::CreateRuleset(error) => write!(f, "{error}"),
            RulesetError::AddRules(error) => write!(f, "{error}"),
            RulesetError::RestrictSelf(error) => write!(f, "{error}"),
        }
    }
}

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

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

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

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

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

    // 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 + std::fmt::Debug,
{
    Compat(CompatError<T>),
}

impl<T> std::fmt::Display for HandleAccessError<T>
where
    T: Access + std::fmt::Debug,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            HandleAccessError::Compat(error) => write!(f, "{error}"),
        }
    }
}

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

impl std::fmt::Display for HandleAccessesError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            HandleAccessesError::Fs(error) => write!(f, "{error}"),
        }
    }
}

impl std::error::Error for HandleAccessesError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            HandleAccessesError::Fs(e) => Some(e),
        }
    }
}

impl std::error::Error for HandleAccessError<AccessFs> {}

// 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 + std::fmt::Debug,
{
    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 std::fmt::Display for CreateRulesetError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            CreateRulesetError::CreateRulesetCall { source } => {
                write!(f, "failed to create a ruleset: {}", source)
            }
            CreateRulesetError::MissingHandledAccess => write!(f, "missing handled access"),
        }
    }
}

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

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

impl<T> std::fmt::Display for AddRuleError<T>
where
    T: Access + std::fmt::Debug,
{
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::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(error) => error.fmt(f),
        }
    }
}

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

impl<A> From<CompatError<A>> for AddRuleError<A>
where
    A: Access + std::fmt::Debug,
{
    fn from(error: CompatError<A>) -> Self {
        AddRuleError::Compat(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>),
}

impl std::fmt::Display for AddRulesError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AddRulesError::Fs(error) => error.fmt(f),
        }
    }
}

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

// 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 + std::fmt::Debug,
{
    fn from(error: AddRuleError<A>) -> Self {
        A::into_add_rules_error(error)
    }
}

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

impl<T> std::fmt::Display for CompatError<T>
where
    T: Access + std::fmt::Debug,
{
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            CompatError::PathBeneath(e) => e.fmt(f),
            CompatError::Access(e) => e.fmt(f),
        }
    }
}

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

impl<T> From<PathBeneathError> for CompatError<T>
where
    T: Access + std::fmt::Debug,
{
    fn from(error: PathBeneathError) -> Self {
        CompatError::PathBeneath(error)
    }
}

impl<T> From<AccessError<T>> for CompatError<T>
where
    T: Access + std::fmt::Debug,
{
    fn from(error: AccessError<T>) -> Self {
        CompatError::Access(error)
    }
}

#[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.
    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: BitFlags<AccessFs>,
        incompatible: BitFlags<AccessFs>,
    },
}

impl std::fmt::Display for PathBeneathError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::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:?}",
                )
            }
        }
    }
}

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

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

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

impl<T> std::error::Error for AccessError<T>
where
    T: Access + std::fmt::Debug,
{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        // For now, none of the variants have an underlying cause.
        None
    }
}

#[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 std::fmt::Display for RestrictSelfError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::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}")
            }
        }
    }
}

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

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

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

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

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

#[cfg(test)]
impl std::fmt::Display for TestRulesetError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TestRulesetError::Ruleset(error) => write!(f, "{error}"),
            TestRulesetError::PathFd(error) => write!(f, "{error}"),
            TestRulesetError::File(error) => write!(f, "{error}"),
        }
    }
}

#[cfg(test)]
impl std::error::Error for TestRulesetError {}

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

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