// indymilter – asynchronous milter library
// Copyright © 2021–2023 David Bürgin <dbuergin@gluet.ch>
//
// This program is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
// details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/>.

use crate::{
    context::{Context, EomContext, NegotiateContext},
    proto_util::{Actions, ProtoOpts, SocketInfo},
};
use bytes::Bytes;
use std::{ffi::CString, future::Future, pin::Pin};

/// The status returned from callbacks.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Status {
    /// Accept this connection or message.
    Accept,
    /// Continue with the next stage.
    Continue,
    /// Reject this connection, message, or recipient.
    Reject,
    /// Reject this connection, message, or recipient with a temporary failure.
    Tempfail,
    /// Silently discard this message.
    Discard,
    /// Do not send a response to the MTA.
    Noreply,
    /// Skip further invocations of this callback.
    Skip,
    /// Enable all stages and all actions during negotiation.
    AllOpts,
}

impl Default for Status {
    fn default() -> Self {
        Self::Continue
    }
}

/// The type returned by milter callbacks.
pub type CallbackFuture<'a> = Pin<Box<dyn Future<Output = Status> + Send + 'a>>;

/// Callback closures for each milter stage.
///
/// Consult the sendmail documentation for the purpose of each callback and the
/// meaning of its arguments. Here is an overview of the callback flow:
///
/// * [`negotiate`][Callbacks::negotiate]
/// * [`connect`][Callbacks::connect]
/// * [`helo`][Callbacks::helo]\*
/// * *for each message:*
///   * [`mail`][Callbacks::mail]
///   * [`rcpt`][Callbacks::rcpt]\*
///   * [`data`][Callbacks::data]
///   * [`header`][Callbacks::header]\*
///   * [`eoh`][Callbacks::eoh]
///   * [`body`][Callbacks::body]\*
///   * [`eom`][Callbacks::eom]
/// * [`close`][Callbacks::close]
///
/// The callbacks indicated may be called repeatedly.
///
/// The SMTP protocol allows multiple messages to be transmitted in a single
/// connection, so the message-scoped callback flow may be traversed multiple
/// times.
///
/// During the message-scoped stages, [`abort`][Callbacks::abort] is called when
/// processing of the current message is aborted.
///
/// The callback [`unknown`][Callbacks::unknown] is called for unknown SMTP
/// commands.
#[derive(Default)]
pub struct Callbacks<T: Send> {
    // Note: Deriving `Default` for `Callbacks` enables struct update syntax
    // (`..Default::default()`) when type parameter `T` is also `Default`.

    /// The callback for the `negotiate` stage.
    pub negotiate:
        Option<Box<dyn for<'cx> Fn(&'cx mut NegotiateContext<T>, Actions, ProtoOpts) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `connect` stage.
    pub connect:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>, CString, SocketInfo) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `helo` stage.
    pub helo:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>, CString) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `mail` stage.
    pub mail:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>, Vec<CString>) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `rcpt` stage.
    pub rcpt:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>, Vec<CString>) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `data` stage.
    pub data:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `header` stage.
    pub header:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>, CString, CString) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `eoh` stage.
    pub eoh:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `body` stage.
    pub body:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>, Bytes) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `eom` stage.
    pub eom:
        Option<Box<dyn for<'cx> Fn(&'cx mut EomContext<T>) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `abort` stage.
    pub abort:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `close` stage.
    pub close:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>) -> CallbackFuture<'cx> + Send + Sync>>,
    /// The callback for the `unknown` stage.
    pub unknown:
        Option<Box<dyn for<'cx> Fn(&'cx mut Context<T>, CString) -> CallbackFuture<'cx> + Send + Sync>>,
}

impl<T: Send> Callbacks<T> {
    /// Returns a new, empty collection of callbacks.
    pub fn new() -> Self {
        Self {
            negotiate: None,
            connect: None,
            helo: None,
            mail: None,
            rcpt: None,
            data: None,
            header: None,
            eoh: None,
            body: None,
            eom: None,
            abort: None,
            close: None,
            unknown: None,
        }
    }

    /// Configures the callback for the `negotiate` stage.
    pub fn on_negotiate(
        mut self,
        callback: impl Fn(&mut NegotiateContext<T>, Actions, ProtoOpts) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.negotiate = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `connect` stage.
    pub fn on_connect(
        mut self,
        callback: impl Fn(&mut Context<T>, CString, SocketInfo) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.connect = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `helo` stage.
    pub fn on_helo(
        mut self,
        callback: impl Fn(&mut Context<T>, CString) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.helo = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `mail` stage.
    pub fn on_mail(
        mut self,
        callback: impl Fn(&mut Context<T>, Vec<CString>) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.mail = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `rcpt` stage.
    pub fn on_rcpt(
        mut self,
        callback: impl Fn(&mut Context<T>, Vec<CString>) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.rcpt = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `data` stage.
    pub fn on_data(
        mut self,
        callback: impl Fn(&mut Context<T>) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.data = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `header` stage.
    pub fn on_header(
        mut self,
        callback: impl Fn(&mut Context<T>, CString, CString) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.header = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `eoh` stage.
    pub fn on_eoh(
        mut self,
        callback: impl Fn(&mut Context<T>) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.eoh = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `body` stage.
    pub fn on_body(
        mut self,
        callback: impl Fn(&mut Context<T>, Bytes) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.body = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `eom` stage.
    pub fn on_eom(
        mut self,
        callback: impl Fn(&mut EomContext<T>) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.eom = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `abort` stage.
    pub fn on_abort(
        mut self,
        callback: impl Fn(&mut Context<T>) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.abort = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `close` stage.
    pub fn on_close(
        mut self,
        callback: impl Fn(&mut Context<T>) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.close = Some(Box::new(callback));
        self
    }

    /// Configures the callback for the `unknown` stage.
    pub fn on_unknown(
        mut self,
        callback: impl Fn(&mut Context<T>, CString) -> CallbackFuture<'_> + Send + Sync + 'static,
    ) -> Self {
        self.unknown = Some(Box::new(callback));
        self
    }
}
