use std::{fmt, io};

use nix::errno::Errno;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCode {
    Exec = 1,
    CapSet = 2,
    ParentDeathSignal = 3,
    PreExec = 4,
    ProcessStop = 5,
    IgnoreSignal = 6,
    ResetSignal = 7,
    SetResourceLimits = 8,
    LandlockFilterScopedSignals = 9,
    Seccomp = 10,
    SeccompFilterIoctl = 11,
    SeccompFilterAppendOnly = 12,
    SeccompFilterKptr = 13,
    SeccompSendFd = 14,
    SeccompWaitFd = 15,
    SetSid = 16,
    SetPty = 17,
    DupPty = 18,
    SetPgid = 19,
    SetPgrp = 20,
    SetTSC = 21,
}

/// Error running process
///
/// This type has very large number of options and it's enum only to be
/// compact. Probably you shouldn't match on the error cases but just format
/// it for user into string.
#[derive(Debug)]
pub enum Error {
    /// Unknown nix error
    ///
    /// Frankly, this error should not happen when running process. We just
    /// keep it here in case `nix` returns this error, which should not happen.
    NixError(i32), // Not sure it's possible, but it is here to convert from
    // nix::Error safer
    /// Some invalid error code received from child application
    UnknownError,
    /// Error when calling capset syscall
    CapSet(i32),
    /// Error when running execve() systemcall
    Exec(i32),
    /// Unable to set death signal (probably signal number invalid)
    ParentDeathSignal(i32),
    /// Before unfreeze callback error
    BeforeUnfreeze(Box<dyn ::std::error::Error + Send + Sync + 'static>),
    /// Before exec callback error
    PreExec(i32),
    /// Error stopping process
    ProcessStop(i32),
    /// Error ignoring signals
    IgnoreSignal(i32),
    /// Error resetting signals
    ResetSignal(i32),
    /// Error setting resource limits
    SetResourceLimits(i32),
    /// Error setting scoped signals using landlock(7)
    LandlockFilterScopedSignals(i32),
    /// Seccomp error (loading filter, getting notify fd)
    Seccomp(i32),
    /// Error filtering ioctl(2) requests with seccomp
    SeccompFilterIoctl(i32),
    /// Error filtering pwritev2(2) requests with seccomp
    SeccompFilterAppendOnly(i32),
    /// Error filtering kernel pointers in syscall arguments with seccomp
    SeccompFilterKptr(i32),
    /// Error sending notification fd through the seccomp sender channel
    SeccompSendFd(i32),
    /// Error waiting for parent to receive the seccomp fd
    SeccompWaitFd(i32),
    /// Error calling setsid(2)
    SetSid(i32),
    /// Error calling TIOCSCTTY ioctl(2)
    SetPty(i32),
    /// Error calling dup(2) on PTY fd
    DupPty(i32),
    /// Error calling setpgid(2)
    SetPgid(i32),
    /// Error calling tcsetpgrp(3)
    SetPgrp(i32),
    /// Error calling prctl PR_SET_TSC
    SetTSC(i32),
}

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

impl Error {
    /// Similarly to `io::Error` returns bare error code
    pub fn raw_os_error(&self) -> Option<i32> {
        use self::Error::*;
        match *self {
            UnknownError => None,
            NixError(x) => Some(x),
            CapSet(x) => Some(x),
            Exec(x) => Some(x),
            ParentDeathSignal(x) => Some(x),
            BeforeUnfreeze(..) => None,
            PreExec(x) => Some(x),
            ProcessStop(x) => Some(x),
            IgnoreSignal(x) => Some(x),
            ResetSignal(x) => Some(x),
            SetResourceLimits(x) => Some(x),
            LandlockFilterScopedSignals(x) => Some(x),
            Seccomp(x) => Some(x),
            SeccompFilterIoctl(x) => Some(x),
            SeccompFilterAppendOnly(x) => Some(x),
            SeccompFilterKptr(x) => Some(x),
            SeccompSendFd(x) => Some(x),
            SeccompWaitFd(x) => Some(x),
            SetSid(x) => Some(x),
            SetPty(x) => Some(x),
            DupPty(x) => Some(x),
            SetPgid(x) => Some(x),
            SetPgrp(x) => Some(x),
            SetTSC(x) => Some(x),
        }
    }
}

impl Error {
    fn title(&self) -> &'static str {
        use self::Error::*;
        match *self {
            UnknownError => "unexpected value received via signal pipe",
            NixError(_) => "some unknown nix error",
            CapSet(_) => "error when setting capabilities",
            Exec(_) => "error when executing",
            ParentDeathSignal(_) => "error when death signal",
            BeforeUnfreeze(_) => "error in before_unfreeze callback",
            PreExec(_) => "error in pre_exec callback",
            ProcessStop(_) => "error stopping process",
            IgnoreSignal(_) => "error ignoring signals",
            ResetSignal(_) => "error resetting signals",
            SetResourceLimits(_) => "error setting resource limits",
            LandlockFilterScopedSignals(_) => "error scoping signals with landlock",
            Seccomp(_) => "error in seccomp filter load",
            SeccompFilterIoctl(_) => "error filtering ioctl requests with seccomp",
            SeccompFilterAppendOnly(_) => "error filtering pwritev2 requests with seccomp",
            SeccompFilterKptr(_) => {
                "error filtering kernel pointers in syscall arguments with seccomp"
            }
            SeccompSendFd(_) => "error sending seccomp file descriptor",
            SeccompWaitFd(_) => "error waiting for parent to receive the seccomp file descriptor",
            SetSid(_) => "error calling setsid",
            SetPty(_) => "error setting pty as controlling terminal",
            DupPty(_) => "error duplicating pty onto stdio fds",
            SetPgid(_) => "error creating new process group",
            SetPgrp(_) => "error setting foreground process group",
            SetTSC(_) => "error setting timestamp counter prctl",
        }
    }
}

impl fmt::Display for Error {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        use crate::unshare::Error::*;
        if let Some(code) = self.raw_os_error() {
            let errno = Errno::from_raw(code);
            if let nix::errno::Errno::UnknownErrno = errno {
                // May be OS knows error name better
                write!(
                    fmt,
                    "{}: {}",
                    self.title(),
                    io::Error::from_raw_os_error(code)
                )
            } else {
                // Format similar to that of std::io::Error
                write!(
                    fmt,
                    "{}: {} (os error {})",
                    self.title(),
                    errno.desc(),
                    code
                )
            }
        } else {
            match self {
                BeforeUnfreeze(err) => {
                    write!(fmt, "{}: {}", self.title(), err)
                }
                _ => write!(fmt, "{}", self.title()),
            }
        }
    }
}
