use crate::{enums::Actions, error::*};
use libc::{c_char, c_uchar, c_ulong, size_t};
use milter_sys as sys;
use std::{convert::TryInto, ffi::CString, ptr, time::Duration};

/// The type of the [`on_negotiate`] callback function pointer.
///
/// [`on_negotiate`]: struct.Milter.html#method.on_negotiate
pub type NegotiateCallback = unsafe extern "C" fn(
    *mut sys::SMFICTX, c_ulong, c_ulong, c_ulong, c_ulong, *mut c_ulong, *mut c_ulong, *mut c_ulong, *mut c_ulong
) -> sys::sfsistat;

/// The type of the [`on_connect`] callback function pointer.
///
/// [`on_connect`]: struct.Milter.html#method.on_connect
pub type ConnectCallback = unsafe extern "C" fn(*mut sys::SMFICTX, *mut c_char, *mut libc::sockaddr) -> sys::sfsistat;

/// The type of the [`on_helo`] callback function pointer.
///
/// [`on_helo`]: struct.Milter.html#method.on_helo
pub type HeloCallback = unsafe extern "C" fn(*mut sys::SMFICTX, *mut c_char) -> sys::sfsistat;

/// The type of the [`on_mail`] callback function pointer.
///
/// [`on_mail`]: struct.Milter.html#method.on_mail
pub type MailCallback = unsafe extern "C" fn(*mut sys::SMFICTX, *mut *mut c_char) -> sys::sfsistat;

/// The type of the [`on_rcpt`] callback function pointer.
///
/// [`on_rcpt`]: struct.Milter.html#method.on_rcpt
pub type RcptCallback = unsafe extern "C" fn(*mut sys::SMFICTX, *mut *mut c_char) -> sys::sfsistat;

/// The type of the [`on_data`] callback function pointer.
///
/// [`on_data`]: struct.Milter.html#method.on_data
pub type DataCallback = unsafe extern "C" fn(*mut sys::SMFICTX) -> sys::sfsistat;

/// The type of the [`on_header`] callback function pointer.
///
/// [`on_header`]: struct.Milter.html#method.on_header
pub type HeaderCallback = unsafe extern "C" fn(*mut sys::SMFICTX, *mut c_char, *mut c_char) -> sys::sfsistat;

/// The type of the [`on_eoh`] callback function pointer.
///
/// [`on_eoh`]: struct.Milter.html#method.on_eoh
pub type EohCallback = unsafe extern "C" fn(*mut sys::SMFICTX) -> sys::sfsistat;

/// The type of the [`on_body`] callback function pointer.
///
/// [`on_body`]: struct.Milter.html#method.on_body
pub type BodyCallback = unsafe extern "C" fn(*mut sys::SMFICTX, *mut c_uchar, size_t) -> sys::sfsistat;

/// The type of the [`on_eom`] callback function pointer.
///
/// [`on_eom`]: struct.Milter.html#method.on_eom
pub type EomCallback = unsafe extern "C" fn(*mut sys::SMFICTX) -> sys::sfsistat;

/// The type of the [`on_abort`] callback function pointer.
///
/// [`on_abort`]: struct.Milter.html#method.on_abort
pub type AbortCallback = unsafe extern "C" fn(*mut sys::SMFICTX) -> sys::sfsistat;

/// The type of the [`on_close`] callback function pointer.
///
/// [`on_close`]: struct.Milter.html#method.on_close
pub type CloseCallback = unsafe extern "C" fn(*mut sys::SMFICTX) -> sys::sfsistat;

/// The type of the [`on_unknown`] callback function pointer.
///
/// [`on_unknown`]: struct.Milter.html#method.on_unknown
pub type UnknownCallback = unsafe extern "C" fn(*mut sys::SMFICTX, *const c_char) -> sys::sfsistat;

/// A configurable milter runner.
///
/// `Milter` is used to configure and run a milter implementation. It is
/// instantiated with [`Milter::new`] and a mandatory listening socket. The
/// milter can then be configured with a [name], a selection of callbacks that
/// provide the functionality of the milter, and the [actions] that the milter
/// wants to take. Finally, a call to [`run`] starts the milter.
///
/// A limitation to be aware of is that while `Milter` appears to expose a
/// tidily encapsulated fluent builder API, **`Milter` must essentially be used
/// like a singleton.** No more than one milter can be configured and run in a
/// single process. (This limitation is due to the design of the underlying
/// milter library, which is based on a paradigm of global state.)
///
/// # Examples
///
/// The following shows the simplest possible, no-op milter.
///
/// ```no_run
/// use milter::Milter;
///
/// Milter::new("unix:/run/noopmilter.sock").run().expect("milter execution failed");
/// ```
///
/// More typically, a milter is configured with a name, some callbacks, and the
/// desired actions before being started:
///
/// ```no_run
/// # use milter::{Actions, Milter};
/// # unsafe extern "C" fn eom_callback(_: *mut milter_sys::SMFICTX) -> milter_sys::sfsistat { 0 }
/// Milter::new("inet:3333@localhost")
///     .name("MyMilter")
///     .on_eom(eom_callback)
///     .actions(Actions::ADD_HEADER)
///     .run()
///     .expect("milter execution failed");
/// ```
///
/// [`Milter::new`]: #method.new
/// [name]: #method.name
/// [actions]: #method.actions
/// [`run`]: #method.run
#[derive(Clone, Debug)]
pub struct Milter {
    socket: CString,
    name: Option<CString>,
    negotiate_callback: Option<NegotiateCallback>,
    connect_callback: Option<ConnectCallback>,
    helo_callback: Option<HeloCallback>,
    mail_callback: Option<MailCallback>,
    rcpt_callback: Option<RcptCallback>,
    data_callback: Option<DataCallback>,
    header_callback: Option<HeaderCallback>,
    eoh_callback: Option<EohCallback>,
    body_callback: Option<BodyCallback>,
    eom_callback: Option<EomCallback>,
    abort_callback: Option<AbortCallback>,
    close_callback: Option<CloseCallback>,
    unknown_callback: Option<UnknownCallback>,
    actions: Actions,
    timeout: Option<i32>,
    socket_backlog: Option<i32>,
    remove_socket: bool,
}

impl Milter {
    /// Creates a new milter that will listen on the given socket.
    ///
    /// The `socket` specification needs to be in one of the following formats:
    ///
    /// * <code>inet:<em>port</em>@<em>host</em></code> – an IPv4 socket
    /// * <code>inet6:<em>port</em>@<em>host</em></code> – an IPv6 socket\
    ///   where `port` is a numeric port, and `host` can be either a hostname or
    ///   an IP address
    /// * <code>{unix|local}:<em>path</em></code> – a UNIX domain socket at an
    ///   absolute file system `path`
    ///
    /// UNIX domain socket files are created using the user and umask of the
    /// milter process.
    ///
    /// While `Milter` is responsible for creation and deletion of UNIX domain
    /// sockets, it is possible that a socket file survives an abnormal
    /// termination. See [`Milter::remove_socket`] for an option that may help
    /// address this.
    ///
    /// # Panics
    ///
    /// Panics if `socket` contains a zero byte.
    ///
    /// # Examples
    ///
    /// ```
    /// # use milter::Milter;
    /// let mut milter = Milter::new("inet:3000@localhost");
    /// ```
    ///
    /// [`Milter::remove_socket`]: #method.remove_socket
    pub fn new(socket: &str) -> Self {
        let socket = CString::new(socket).expect("zero byte in socket specification");

        Self {
            socket,
            name: None,
            negotiate_callback: None,
            connect_callback: None,
            helo_callback: None,
            mail_callback: None,
            rcpt_callback: None,
            data_callback: None,
            header_callback: None,
            eoh_callback: None,
            body_callback: None,
            eom_callback: None,
            abort_callback: None,
            close_callback: None,
            unknown_callback: None,
            actions: Default::default(),
            timeout: None,
            socket_backlog: None,
            remove_socket: false,
        }
    }

    /// Configures a descriptive name for this milter.
    ///
    /// # Panics
    ///
    /// Panics if `name` contains a zero byte.
    pub fn name(&mut self, name: &str) -> &mut Self {
        self.name = Some(CString::new(name).expect("zero byte in milter name"));
        self
    }

    /// Sets the callback to use for this milter’s `negotiate` stage.
    ///
    /// Use the [`on_negotiate`] attribute macro to generate the callback
    /// function.
    ///
    /// [`on_negotiate`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_negotiate.html
    pub fn on_negotiate(&mut self, callback: NegotiateCallback) -> &mut Self {
        self.negotiate_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `connect` stage.
    ///
    /// Use the [`on_connect`] attribute macro to generate the callback
    /// function.
    ///
    /// [`on_connect`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_connect.html
    pub fn on_connect(&mut self, callback: ConnectCallback) -> &mut Self {
        self.connect_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `helo` stage.
    ///
    /// Use the [`on_helo`] attribute macro to generate the callback function.
    ///
    /// [`on_helo`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_helo.html
    pub fn on_helo(&mut self, callback: HeloCallback) -> &mut Self {
        self.helo_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `mail` stage.
    ///
    /// Use the [`on_mail`] attribute macro to generate the callback function.
    ///
    /// [`on_mail`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_mail.html
    pub fn on_mail(&mut self, callback: MailCallback) -> &mut Self {
        self.mail_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `rcpt` stage.
    ///
    /// Use the [`on_rcpt`] attribute macro to generate the callback function.
    ///
    /// [`on_rcpt`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_rcpt.html
    pub fn on_rcpt(&mut self, callback: RcptCallback) -> &mut Self {
        self.rcpt_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `data` stage.
    ///
    /// Use the [`on_data`] attribute macro to generate the callback function.
    ///
    /// [`on_data`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_data.html
    pub fn on_data(&mut self, callback: DataCallback) -> &mut Self {
        self.data_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `header` stage.
    ///
    /// Use the [`on_header`] attribute macro to generate the callback function.
    ///
    /// [`on_header`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_header.html
    pub fn on_header(&mut self, callback: HeaderCallback) -> &mut Self {
        self.header_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `eoh` (end-of-header) stage.
    ///
    /// Use the [`on_eoh`] attribute macro to generate the callback function.
    ///
    /// [`on_eoh`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_eoh.html
    pub fn on_eoh(&mut self, callback: EohCallback) -> &mut Self {
        self.eoh_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `body` stage.
    ///
    /// Use the [`on_body`] attribute macro to generate the callback function.
    ///
    /// [`on_body`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_body.html
    pub fn on_body(&mut self, callback: BodyCallback) -> &mut Self {
        self.body_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `eom` (end-of-message) stage.
    ///
    /// Use the [`on_eom`] attribute macro to generate the callback function.
    ///
    /// [`on_eom`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_eom.html
    pub fn on_eom(&mut self, callback: EomCallback) -> &mut Self {
        self.eom_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `abort` stage.
    ///
    /// Use the [`on_abort`] attribute macro to generate the callback function.
    ///
    /// [`on_abort`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_abort.html
    pub fn on_abort(&mut self, callback: AbortCallback) -> &mut Self {
        self.abort_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `close` stage.
    ///
    /// Use the [`on_close`] attribute macro to generate the callback function.
    ///
    /// [`on_close`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_close.html
    pub fn on_close(&mut self, callback: CloseCallback) -> &mut Self {
        self.close_callback = Some(callback);
        self
    }

    /// Sets the callback to use for this milter’s `unknown` stage.
    ///
    /// Use the [`on_unknown`] attribute macro to generate the callback
    /// function.
    ///
    /// [`on_unknown`]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_unknown.html
    pub fn on_unknown(&mut self, callback: UnknownCallback) -> &mut Self {
        self.unknown_callback = Some(callback);
        self
    }

    /// Enables the given actions for this milter.
    ///
    /// Actions represented as methods on [`ContextApi`] need to be enabled with
    /// this method (or during [negotiation]) before they can be used.
    ///
    /// [`ContextApi`]: struct.ContextApi.html
    /// [negotiation]: https://docs.rs/milter-callback/0.2.2/milter_callback/attr.on_negotiate.html
    pub fn actions(&mut self, actions: Actions) -> &mut Self {
        self.actions = actions;
        self
    }

    /// Configures the time until the connection to the MTA times out.
    ///
    /// # Panics
    ///
    /// Panics if the duration is outside the acceptable range.
    pub fn timeout(&mut self, duration: Duration) -> &mut Self {
        self.timeout = Some(duration.as_secs().try_into().expect("timeout duration out of range"));
        self
    }

    /// Configures the socket backlog, that is, the maximum number of incoming
    /// connections to queue up.
    ///
    /// # Panics
    ///
    /// Panics if the given length is outside the acceptable range.
    pub fn socket_backlog(&mut self, len: u32) -> &mut Self {
        self.socket_backlog = Some(len.try_into().expect("socket backlog length out of range"));
        self
    }

    /// Configures whether to force removal of any existing UNIX domain socket
    /// at the location given in [`Milter::new`] during startup.
    ///
    /// While a `Milter` normally manages creation and deletion of socket files
    /// itself, a socket file may survive an abnormal termination; enabling this
    /// option may help clear out leftover files when (re-)starting.
    ///
    /// [`Milter::new`]: #method.new
    pub fn remove_socket(&mut self, value: bool) -> &mut Self {
        self.remove_socket = value;
        self
    }

    /// Registers this milter’s configuration with the milter library and starts
    /// the milter. This is a blocking call, `run` only returns when the milter
    /// is shut down.
    ///
    /// **Important:** Not more than one milter may be configured and run at a
    /// time.
    ///
    /// # Errors
    ///
    /// Only when calling `run` is this milter’s configuration applied. This can
    /// fail for a variety of reasons encoded in the error result.
    pub fn run(&self) -> Result<()> {
        unsafe {
            self.register()?;

            crate::internal::set_panicked(false);

            match sys::smfi_main().into() {
                ReturnCode::Success => {
                    if crate::internal::is_panicked() {
                        Err(Error::CallbackPanic)
                    } else {
                        Ok(())
                    }
                }
                ReturnCode::Failure => Err(Error::Main),
            }
        }
    }

    unsafe fn register(&self) -> Result<()> {
        if let ReturnCode::Failure = sys::smfi_setconn(self.socket.as_ptr() as _).into() {
            return Err(Error::SocketConfig(self.socket.clone()));
        }

        let descriptor = sys::smfiDesc {
            xxfi_name: self.name.as_ref().map_or(ptr::null_mut(), |s| s.as_ptr() as _),
            xxfi_version: sys::SMFI_VERSION,
            xxfi_flags: self.actions.bits(),
            xxfi_connect: self.connect_callback,
            xxfi_helo: self.helo_callback,
            xxfi_envfrom: self.mail_callback,
            xxfi_envrcpt: self.rcpt_callback,
            xxfi_header: self.header_callback,
            xxfi_eoh: self.eoh_callback,
            xxfi_body: self.body_callback,
            xxfi_eom: self.eom_callback,
            xxfi_abort: self.abort_callback,
            xxfi_close: self.close_callback,
            xxfi_unknown: self.unknown_callback,
            xxfi_data: self.data_callback,
            xxfi_negotiate: self.negotiate_callback,
        };

        if let ReturnCode::Failure = sys::smfi_register(descriptor).into() {
            return Err(Error::Registration);
        }

        if let ReturnCode::Failure = sys::smfi_opensocket(self.remove_socket as _).into() {
            return Err(Error::SocketConfig(self.socket.clone()));
        }

        if let Some(timeout) = self.timeout {
            if let ReturnCode::Failure = sys::smfi_settimeout(timeout).into() {
                return Err(Error::TimeoutConfig(timeout));
            }
        }
        if let Some(socket_backlog) = self.socket_backlog {
            if let ReturnCode::Failure = sys::smfi_setbacklog(socket_backlog).into() {
                return Err(Error::SocketBacklogConfig(socket_backlog));
            }
        }

        Ok(())
    }
}

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

    #[test]
    fn test_milter() {
        let mut milter = Milter::new("unix:/run/miltertest.sock");
        milter
            .name("test")
            .timeout(Duration::from_secs(30))
            .socket_backlog(64);
    }
}
