use crate::{enums::Stage, error::*};
use libc::c_char;
use milter_sys as sys;
use once_cell::unsync::OnceCell;
use std::{
    convert::TryInto,
    ffi::{CStr, CString},
    mem,
    ptr::{self, NonNull},
    result,
};

/// Context supplied to the milter callbacks.
///
/// The `Context<T>` struct provides access to the context API via its [`api`]
/// field. It also provides access to connection-local user [`data`], the type
/// of which is `T`.
///
/// # Safety
///
/// The type parameter `T` specifies the type of the value to be retrieved from
/// the `data` handle. Retrieving the data is potentially an unsafe operation,
/// as the data needs to be materialised from a raw C pointer. Take care not to
/// introduce a mismatch when specifying `T`; `T` should always be the same type
/// over the whole callback flow.
///
/// [`api`]: #structfield.api
/// [`data`]: #structfield.data
#[derive(Debug)]
pub struct Context<T> {
    /// An accessor to the API methods operating on this context.
    pub api: ContextApi,

    /// A handle on user data associated with this context.
    pub data: DataHandle<T>,
}

impl<T> Context<T> {
    /// Constructs a new `Context` from the milter library-supplied raw context
    /// pointer.
    ///
    /// You do not normally need to use `new`; a `Context` is already supplied
    /// to the callbacks.
    ///
    /// # Panics
    ///
    /// Panics if `ptr` is null.
    pub fn new(ptr: *mut sys::SMFICTX) -> Self {
        assert!(!ptr.is_null());

        Self {
            api: ContextApi::new(ptr),
            data: DataHandle::new(ptr),
        }
    }
}

/// An accessor to the set of methods that make up the context API.
///
/// The context API is the set of mostly side-effecting operations that affect
/// the current connection.
///
/// # Action methods
///
/// Of the methods that are part of the context API, some are designated *action
/// methods*. These are the methods that modify the message being processed.
/// They can be called only in the [`eom`] milter callback, and must be enabled
/// before use, either using [`Milter::actions`] or during [negotiation].
///
/// The trait [`ActionContext`] serves as a representation of the set of action
/// methods.
///
/// [`eom`]: struct.Milter.html#method.on_eom
/// [`Milter::actions`]: struct.Milter.html#method.actions
/// [negotiation]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_negotiate.html
/// [`ActionContext`]: trait.ActionContext.html
#[derive(Debug)]
pub struct ContextApi {
    context_ptr: NonNull<sys::SMFICTX>,
}

impl ContextApi {
    fn new(ptr: *mut sys::SMFICTX) -> Self {
        assert!(!ptr.is_null());

        Self {
            context_ptr: NonNull::new(ptr).unwrap(),
        }
    }

    /// Requests that the space-separated `macros` be made available in the
    /// given stage. The set of macros appropriate to use depends on the MTA.
    /// Passing the empty string requests that no macros be sent. This method
    /// may only be called once for each `Stage` variant; calls per stage are
    /// not cumulative.
    ///
    /// This method is part of the [negotiation] procedure and should therefore
    /// only be called in the `negotiate` stage.
    ///
    /// This action is enabled with the flag [`Actions::REQUEST_MACROS`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the underlying milter library returns a failure status.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # use milter::Stage;
    /// # let context = milter::Context::<()>::new(std::ptr::null_mut());
    /// context.api.request_macros(Stage::Helo, "{cert_issuer} {cert_subject}")?;
    /// # Ok::<_, milter::Error>(())
    /// ```
    ///
    /// [negotiation]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_negotiate.html
    /// [`Actions::REQUEST_MACROS`]: struct.Actions.html#associatedconstant.REQUEST_MACROS
    pub fn request_macros(&self, stage: Stage, macros: &str) -> Result<()> {
        let macros = CString::new(macros)?;

        let ret_code = unsafe {
            sys::smfi_setsymlist(self.context_ptr.as_ptr(), stage as _, macros.as_ptr() as _)
        };

        ReturnCode::from(ret_code).into()
    }

    /// Returns the value for the given macro if present. The set of macros
    /// available depends on the stage and the MTA in use.
    ///
    /// # Errors
    ///
    /// If conversion of arguments or return values at the boundary to the
    /// milter library fails, an error variant is returned.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # let context = milter::Context::<i32>::new(std::ptr::null_mut());
    /// let ip_address = context.api.macro_value("{daemon_addr}")?;
    /// # Ok::<_, milter::Error>(())
    /// ```
    pub fn macro_value(&self, name: &str) -> Result<Option<&str>> {
        let name = CString::new(name)?;

        unsafe {
            let value = sys::smfi_getsymval(self.context_ptr.as_ptr(), name.as_ptr() as _);

            Ok(if value.is_null() {
                None
            } else {
                Some(CStr::from_ptr(value).to_str()?)
            })
        }
    }

    /// Sets the default SMTP error reply code for the current connection.
    ///
    /// The three-digit SMTP reply code (RFC 5321) may be enhanced with an
    /// optional extended status code (RFC 3463), and with an optional
    /// multi-line text message.
    ///
    /// This setting only takes effect when the first digit of the reply code
    /// matches the `Status` returned from the callback: `"4xx"` for
    /// [`Tempfail`], `"5xx"` for [`Reject`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the underlying milter library returns a failure status.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # let context = milter::Context::<()>::new(std::ptr::null_mut());
    /// context.api.set_error_reply("550", Some("5.7.0"), vec![
    ///     "Access rejected.",
    ///     "Rejection is due to the sending MTA's poor reputation.",
    /// ])?;
    /// # Ok::<_, milter::Error>(())
    /// ```
    ///
    /// [`Tempfail`]: enum.Status.html#variant.Tempfail
    /// [`Reject`]: enum.Status.html#variant.Reject
    pub fn set_error_reply(
        &self,
        code: &str,
        ext_code: Option<&str>,
        msg_lines: Vec<&str>,
    ) -> Result<()> {
        let code = CString::new(code)?;
        let ext_code = ext_code.map(CString::new).transpose()?;

        let l: Vec<CString> = msg_lines
            .into_iter()
            .map(CString::new)
            .collect::<result::Result<_, _>>()?;

        let p = self.context_ptr.as_ptr();
        let c = code.as_ptr() as *mut _;
        let x = ext_code.as_ref().map_or(ptr::null_mut(), |s| s.as_ptr() as *mut _);

        // `smfi_setmlreply` is a varargs function. Fortunately it limits its
        // argument count to 32, so we can actually exhaustively match and
        // expand the arities below.

        macro_rules! set_reply {
            ($line:expr) => {
                sys::smfi_setreply(p, c, x, $line as _)
            };
            ($($line:expr),+) => {
                sys::smfi_setmlreply(
                    p, c, x, $($line.as_ptr() as *mut c_char),+, ptr::null_mut() as *mut c_char
                )
            }
        }

        let ret_code = unsafe {
            match l.len() {
                0 => set_reply!(ptr::null_mut()),
                1 => set_reply!(l[0].as_ptr()),
                2 => set_reply!(l[0], l[1]),
                3 => set_reply!(l[0], l[1], l[2]),
                4 => set_reply!(l[0], l[1], l[2], l[3]),
                5 => set_reply!(l[0], l[1], l[2], l[3], l[4]),
                6 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5]),
                7 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6]),
                8 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7]),
                9 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8]),
                10 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9]),
                11 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10]),
                12 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11]),
                13 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12]),
                14 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13]),
                15 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14]),
                16 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15]),
                17 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16]),
                18 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17]),
                19 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18]),
                20 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19]),
                21 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20]),
                22 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21]),
                23 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22]),
                24 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23]),
                25 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23], l[24]),
                26 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23], l[24], l[25]),
                27 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23], l[24], l[25], l[26]),
                28 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23], l[24], l[25], l[26], l[27]),
                29 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23], l[24], l[25], l[26], l[27], l[28]),
                30 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23], l[24], l[25], l[26], l[27], l[28], l[29]),
                31 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23], l[24], l[25], l[26], l[27], l[28], l[29], l[30]),
                32 => set_reply!(l[0], l[1], l[2], l[3], l[4], l[5], l[6], l[7], l[8], l[9], l[10], l[11], l[12], l[13], l[14], l[15], l[16], l[17], l[18], l[19], l[20], l[21], l[22], l[23], l[24], l[25], l[26], l[27], l[28], l[29], l[30], l[31]),
                _ => panic!("more than 32 message lines"),
            }
        };

        ReturnCode::from(ret_code).into()
    }

    /// Replaces the envelope sender (`MAIL FROM` address) of the current
    /// message with `mail_from`. Optional additional ESMTP arguments to the
    /// `MAIL` command may be given in `esmtp_args`.
    ///
    /// This action is enabled with the flag [`Actions::REPLACE_SENDER`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the milter library returns a failure status.
    ///
    /// [`Actions::REPLACE_SENDER`]: struct.Actions.html#associatedconstant.REPLACE_SENDER
    pub fn replace_sender(&self, mail_from: &str, esmtp_args: Option<&str>) -> Result<()> {
        let mail_from = CString::new(mail_from)?;
        let esmtp_args = esmtp_args.map(CString::new).transpose()?;

        let ret_code = unsafe {
            sys::smfi_chgfrom(
                self.context_ptr.as_ptr(),
                mail_from.as_ptr() as _,
                esmtp_args.as_ref().map_or(ptr::null_mut(), |s| s.as_ptr() as _),
            )
        };

        ReturnCode::from(ret_code).into()
    }

    /// Adds envelope recipient (`RCPT TO` address) `rcpt_to` to the current
    /// message. Optional additional ESMTP arguments to the `RCPT` command may
    /// be given in `esmtp_args`.
    ///
    /// This action is enabled with two distinct flags:
    ///
    /// * [`Actions::ADD_RECIPIENT`] if no ESMTP arguments are specified
    /// * [`Actions::ADD_RECIPIENT_EXT`] if additional ESMTP arguments are
    ///   specified
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the milter library returns a failure status.
    ///
    /// [`Actions::ADD_RECIPIENT`]: struct.Actions.html#associatedconstant.ADD_RECIPIENT
    /// [`Actions::ADD_RECIPIENT_EXT`]: struct.Actions.html#associatedconstant.ADD_RECIPIENT_EXT
    pub fn add_recipient(&self, rcpt_to: &str, esmtp_args: Option<&str>) -> Result<()> {
        let rcpt_to = CString::new(rcpt_to)?;

        let ret_code = unsafe {
            match esmtp_args {
                None => sys::smfi_addrcpt(self.context_ptr.as_ptr(), rcpt_to.as_ptr() as _),
                Some(esmtp_args) => {
                    let esmtp_args = CString::new(esmtp_args)?;

                    sys::smfi_addrcpt_par(
                        self.context_ptr.as_ptr(),
                        rcpt_to.as_ptr() as _,
                        esmtp_args.as_ptr() as _,
                    )
                }
            }
        };

        ReturnCode::from(ret_code).into()
    }

    /// Removes envelope recipient (`RCPT TO` address) `rcpt_to` from the
    /// current message.
    ///
    /// This action is enabled with the flag [`Actions::REMOVE_RECIPIENT`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the milter library returns a failure status.
    ///
    /// [`Actions::REMOVE_RECIPIENT`]: struct.Actions.html#associatedconstant.REMOVE_RECIPIENT
    pub fn remove_recipient(&self, rcpt_to: &str) -> Result<()> {
        let rcpt_to = CString::new(rcpt_to)?;

        let ret_code = unsafe {
            sys::smfi_delrcpt(self.context_ptr.as_ptr(), rcpt_to.as_ptr() as _)
        };

        ReturnCode::from(ret_code).into()
    }

    /// Appends a header to the list of headers of the current message. If the
    /// header value is to span multiple lines, use `\n` (followed by
    /// whitespace) as the line separator, not `\r\n`.
    ///
    /// This action is enabled with the flag [`Actions::ADD_HEADER`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the milter library returns a failure status.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # let context = milter::Context::<()>::new(std::ptr::null_mut());
    /// context.api.add_header(
    ///     "Content-Type",
    ///     "multipart/signed; micalg=pgp-sha512;\n\
    ///      \tprotocol=\"application/pgp-signature\"; boundary=\"=-=-=\"",
    /// )?;
    /// # Ok::<_, milter::Error>(())
    /// ```
    ///
    /// [`Actions::ADD_HEADER`]: struct.Actions.html#associatedconstant.ADD_HEADER
    pub fn add_header(&self, name: &str, value: &str) -> Result<()> {
        let name = CString::new(name)?;
        let value = CString::new(value)?;

        let ret_code = unsafe {
            sys::smfi_addheader(self.context_ptr.as_ptr(), name.as_ptr() as _, value.as_ptr() as _)
        };

        ReturnCode::from(ret_code).into()
    }

    /// Inserts a header at `index` in the list of headers of the current
    /// message. The index is 1-based (starts at 1). If the header value is to
    /// span multiple lines, use `\n` (followed by whitespace) as the line
    /// separator, not `\r\n`.
    ///
    /// This action is enabled with the flag [`Actions::ADD_HEADER`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the milter library returns a failure status.
    ///
    /// [`Actions::ADD_HEADER`]: struct.Actions.html#associatedconstant.ADD_HEADER
    pub fn insert_header(&self, index: usize, name: &str, value: &str) -> Result<()> {
        let index = index.try_into()?;
        let name = CString::new(name)?;
        let value = CString::new(value)?;

        let ret_code = unsafe {
            sys::smfi_insheader(
                self.context_ptr.as_ptr(),
                index,
                name.as_ptr() as _,
                value.as_ptr() as _,
            )
        };

        ReturnCode::from(ret_code).into()
    }

    /// Replaces (or removes, or appends) a header at the `index`’th occurrence
    /// of headers with the given `name` for the current message. The index is
    /// 1-based (starts at 1).
    ///
    /// More precisely,
    ///
    /// * replaces the `index`’th occurrence of headers named `name` with the
    ///   specified value;
    /// * if the value is `None`, the header is instead removed;
    /// * if index is greater than the number of occurrences of headers named
    ///   `name`, a new header is instead appended.
    ///
    /// If the header value is to span multiple lines, use `\n` (followed by
    /// whitespace) as the line separator, not `\r\n`.
    ///
    /// This action is enabled with the flag [`Actions::REPLACE_HEADER`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the milter library returns a failure status.
    ///
    /// [`Actions::REPLACE_HEADER`]: struct.Actions.html#associatedconstant.REPLACE_HEADER
    pub fn replace_header(&self, name: &str, index: usize, value: Option<&str>) -> Result<()> {
        let name = CString::new(name)?;
        let index = index.try_into()?;
        let value = value.map(CString::new).transpose()?;

        let ret_code = unsafe {
            sys::smfi_chgheader(
                self.context_ptr.as_ptr(),
                name.as_ptr() as _,
                index,
                value.as_ref().map_or(ptr::null_mut(), |s| s.as_ptr() as _),
            )
        };

        ReturnCode::from(ret_code).into()
    }

    /// Appends a chunk of bytes to the new message body of the current message.
    ///
    /// This method may be called repeatedly: initially empty, the new body is
    /// augmented with additional content with each call. If this method is not
    /// called, the original message body remains unchanged.
    ///
    /// This action is enabled with the flag [`Actions::REPLACE_BODY`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if the milter library returns a failure
    /// status.
    ///
    /// [`Actions::REPLACE_BODY`]: struct.Actions.html#associatedconstant.REPLACE_BODY
    pub fn append_body_chunk(&self, content: &[u8]) -> Result<()> {
        let ret_code = unsafe {
            sys::smfi_replacebody(
                self.context_ptr.as_ptr(),
                content.as_ptr() as _,
                content.len() as _,
            )
        };

        ReturnCode::from(ret_code).into()
    }

    /// Quarantines the current message for the given reason.
    ///
    /// This action is enabled with the flag [`Actions::QUARANTINE`].
    ///
    /// # Errors
    ///
    /// An error variant is returned if type conversion at the FFI boundary
    /// fails, or if the milter library returns a failure status.
    ///
    /// [`Actions::QUARANTINE`]: struct.Actions.html#associatedconstant.QUARANTINE
    pub fn quarantine(&self, reason: &str) -> Result<()> {
        let reason = CString::new(reason)?;

        let ret_code = unsafe {
            sys::smfi_quarantine(self.context_ptr.as_ptr(), reason.as_ptr() as _)
        };

        ReturnCode::from(ret_code).into()
    }

    /// Signals to the milter library that this milter is still active, causing
    /// it to reset timeouts.
    ///
    /// # Errors
    ///
    /// An error variant is returned if the milter library returns a failure
    /// status.
    pub fn signal_progress(&self) -> Result<()> {
        let ret_code = unsafe { sys::smfi_progress(self.context_ptr.as_ptr()) };

        ReturnCode::from(ret_code).into()
    }
}

/// A trait for macro lookup.
pub trait MacroValue {
    /// Returns the value for the given macro as specified for
    /// [`ContextApi::macro_value`].
    ///
    /// [`ContextApi::macro_value`]: struct.ContextApi.html#method.macro_value
    fn macro_value(&self, name: &str) -> Result<Option<&str>>;
}

impl MacroValue for ContextApi {
    fn macro_value(&self, name: &str) -> Result<Option<&str>> {
        Self::macro_value(self, name)
    }
}

/// A trait for setting up a custom SMTP error reply.
pub trait SetErrorReply {
    /// Sets the SMTP error reply as specified for
    /// [`ContextApi::set_error_reply`].
    ///
    /// [`ContextApi::set_error_reply`]: struct.ContextApi.html#method.set_error_reply
    fn set_error_reply(&self, code: &str, ext_code: Option<&str>, msg_lines: Vec<&str>) -> Result<()>;
}

impl SetErrorReply for ContextApi {
    fn set_error_reply(&self, code: &str, ext_code: Option<&str>, msg_lines: Vec<&str>) -> Result<()> {
        Self::set_error_reply(self, code, ext_code, msg_lines)
    }
}

/// A trait encapsulating the set of action methods available during the `eom`
/// stage.
pub trait ActionContext {
    /// Replaces the envelope sender of a message as specified for
    /// [`ContextApi::replace_sender`].
    ///
    /// [`ContextApi::replace_sender`]: struct.ContextApi.html#method.replace_sender
    fn replace_sender(&self, mail_from: &str, esmtp_args: Option<&str>) -> Result<()>;

    /// Adds an envelope recipient to a message as specified for
    /// [`ContextApi::add_recipient`].
    ///
    /// [`ContextApi::add_recipient`]: struct.ContextApi.html#method.add_recipient
    fn add_recipient(&self, rcpt_to: &str, esmtp_args: Option<&str>) -> Result<()>;

    /// Removes an envelope recipient from a message as specified for
    /// [`ContextApi::remove_recipient`].
    ///
    /// [`ContextApi::remove_recipient`]: struct.ContextApi.html#method.remove_recipient
    fn remove_recipient(&self, rcpt_to: &str) -> Result<()>;

    /// Appends a header to the list of headers of a message as specified for
    /// [`ContextApi::add_header`].
    ///
    /// [`ContextApi::add_header`]: struct.ContextApi.html#method.add_header
    fn add_header(&self, name: &str, value: &str) -> Result<()>;

    /// Inserts a header into the list of headers of a message as specified for
    /// [`ContextApi::insert_header`].
    ///
    /// [`ContextApi::insert_header`]: struct.ContextApi.html#method.insert_header
    fn insert_header(&self, index: usize, name: &str, value: &str) -> Result<()>;

    /// Replaces a header in the list of headers of a message as specified for
    /// [`ContextApi::replace_header`].
    ///
    /// [`ContextApi::replace_header`]: struct.ContextApi.html#method.replace_header
    fn replace_header(&self, name: &str, index: usize, value: Option<&str>) -> Result<()>;

    /// Appends a chunk of bytes to the new message body of a message as
    /// specified for [`ContextApi::append_body_chunk`].
    ///
    /// [`ContextApi::append_body_chunk`]: struct.ContextApi.html#method.append_body_chunk
    fn append_body_chunk(&self, content: &[u8]) -> Result<()>;

    /// Quarantines a message as specified for [`ContextApi::quarantine`].
    ///
    /// [`ContextApi::quarantine`]: struct.ContextApi.html#method.quarantine
    fn quarantine(&self, reason: &str) -> Result<()>;

    /// Signals to the milter library that work is in progress as specified for
    /// [`ContextApi::signal_progress`].
    ///
    /// [`ContextApi::signal_progress`]: struct.ContextApi.html#method.signal_progress
    fn signal_progress(&self) -> Result<()>;
}

impl ActionContext for ContextApi {
    fn replace_sender(&self, mail_from: &str, esmtp_args: Option<&str>) -> Result<()> {
        Self::replace_sender(self, mail_from, esmtp_args)
    }

    fn add_recipient(&self, rcpt_to: &str, esmtp_args: Option<&str>) -> Result<()> {
        Self::add_recipient(self, rcpt_to, esmtp_args)
    }

    fn remove_recipient(&self, rcpt_to: &str) -> Result<()> {
        Self::remove_recipient(self, rcpt_to)
    }

    fn add_header(&self, name: &str, value: &str) -> Result<()> {
        Self::add_header(self, name, value)
    }

    fn insert_header(&self, index: usize, name: &str, value: &str) -> Result<()> {
        Self::insert_header(self, index, name, value)
    }

    fn replace_header(&self, name: &str, index: usize, value: Option<&str>) -> Result<()> {
        Self::replace_header(self, name, index, value)
    }

    fn append_body_chunk(&self, content: &[u8]) -> Result<()> {
        Self::append_body_chunk(self, content)
    }

    fn quarantine(&self, reason: &str) -> Result<()> {
        Self::quarantine(self, reason)
    }

    fn signal_progress(&self) -> Result<()> {
        Self::signal_progress(self)
    }
}

/// A handle on user data stored in the callback context.
///
/// `DataHandle<T>` serves as an accessor to connection-local data `T` that can
/// be shared across callback functions within a connection.
///
/// # Data life cycle
///
/// By design, data life cycle management is not automatic. It is the
/// responsibility of a milter implementation to manage the data’s life cycle by
/// reacquiring and disposing of context data (including associated resources
/// such as file handles etc.) at an appropriate time in the callback flow.
///
/// As a rule of thumb, **every path through the callback flow must have a final
/// call to [`take`].**
///
/// [`take`]: #method.take
#[derive(Debug)]
pub struct DataHandle<T> {
    context_ptr: NonNull<sys::SMFICTX>,
    data_ptr: OnceCell<Option<NonNull<T>>>,
}

impl<T> DataHandle<T> {
    fn new(ptr: *mut sys::SMFICTX) -> Self {
        assert!(!ptr.is_null());

        Self {
            context_ptr: NonNull::new(ptr).unwrap(),
            data_ptr: OnceCell::new(),
        }
    }

    /// Hands over data into the context, returning current context data if
    /// present.
    ///
    /// # Errors
    ///
    /// An error variant is returned if the milter library encounters an error.
    pub fn replace(&mut self, data: T) -> Result<Option<T>> {
        unsafe { self.replace_data(Box::into_raw(Box::new(data))) }
    }

    /// Takes ownership of the current context data if present, removing it from
    /// the context.
    ///
    /// # Errors
    ///
    /// An error variant is returned if the milter library encounters an error.
    pub fn take(&mut self) -> Result<Option<T>> {
        unsafe { self.replace_data(ptr::null_mut()) }
    }

    unsafe fn replace_data(&mut self, ptr: *mut T) -> Result<Option<T>> {
        self.get_or_init_data();

        let ret_code = sys::smfi_setpriv(self.context_ptr.as_ptr(), ptr as _).into();

        match ret_code {
            ReturnCode::Success => {
                Ok(mem::replace(self.data_ptr.get_mut().unwrap(), NonNull::new(ptr))
                    .map(|x| *Box::from_raw(x.as_ptr())))
            }
            ReturnCode::Failure => {
                // On failure, resurrect the data and drop it.
                if !ptr.is_null() {
                    Box::from_raw(ptr);
                }
                Err(Error::FailureStatus)
            }
        }
    }

    /// Returns a reference to (borrows) the current context data if present.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # let context = milter::Context::<i32>::new(std::ptr::null_mut());
    /// if let Some(data) = context.data.borrow() {
    ///     println!("{}", data);
    /// }
    /// # Ok::<_, milter::Error>(())
    /// ```
    pub fn borrow(&self) -> Option<&T> {
        self.get_or_init_data().as_ref().map(|x| unsafe { x.as_ref() })
    }

    /// Returns a mutable reference to (mutably borrows) the current context
    /// data if present.
    ///
    /// # Examples
    ///
    /// ```no_run
    /// # let mut context = milter::Context::<usize>::new(std::ptr::null_mut());
    /// if let Some(msg_count) = context.data.borrow_mut() {
    ///     *msg_count += 1;
    /// }
    /// # Ok::<_, milter::Error>(())
    /// ```
    pub fn borrow_mut(&mut self) -> Option<&mut T> {
        self.get_or_init_data();

        self.data_ptr.get_mut().unwrap().as_mut().map(|x| unsafe { x.as_mut() })
    }

    fn get_or_init_data(&self) -> &Option<NonNull<T>> {
        self.data_ptr.get_or_init(|| {
            NonNull::new(unsafe { sys::smfi_getpriv(self.context_ptr.as_ptr()) as _ })
        })
    }
}
