use std::{ffi::OsString, num::NonZeroU16, sync::Arc};
use async_trait::async_trait;
use lettre::{
    address::Envelope,
    transport::{
        sendmail::AsyncSendmailTransport,
        smtp::{authentication::Credentials, AsyncSmtpTransport},
    },
    AsyncTransport, Tokio1Executor,
};
use thiserror::Error;
#[derive(Debug, Clone, Copy)]
pub enum SmtpMode {
    Plain,
    StartTls,
    Tls,
}
#[derive(Default, Clone)]
pub struct Transport {
    inner: Arc<TransportInner>,
}
enum TransportInner {
    Blackhole,
    Smtp(AsyncSmtpTransport<Tokio1Executor>),
    Sendmail(AsyncSendmailTransport<Tokio1Executor>),
}
impl Transport {
    fn new(inner: TransportInner) -> Self {
        let inner = Arc::new(inner);
        Self { inner }
    }
    #[must_use]
    pub fn blackhole() -> Self {
        Self::new(TransportInner::Blackhole)
    }
    pub fn smtp(
        mode: SmtpMode,
        hostname: &str,
        port: Option<NonZeroU16>,
        credentials: Option<Credentials>,
    ) -> Result<Self, lettre::transport::smtp::Error> {
        let mut t = match mode {
            SmtpMode::Plain => AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(hostname),
            SmtpMode::StartTls => AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(hostname)?,
            SmtpMode::Tls => AsyncSmtpTransport::<Tokio1Executor>::relay(hostname)?,
        };
        if let Some(credentials) = credentials {
            t = t.credentials(credentials);
        }
        if let Some(port) = port {
            t = t.port(port.into());
        }
        Ok(Self::new(TransportInner::Smtp(t.build())))
    }
    #[must_use]
    pub fn sendmail(command: Option<impl Into<OsString>>) -> Self {
        let transport = if let Some(command) = command {
            AsyncSendmailTransport::new_with_command(command)
        } else {
            AsyncSendmailTransport::new()
        };
        Self::new(TransportInner::Sendmail(transport))
    }
}
impl Transport {
    pub async fn test_connection(&self) -> Result<(), Error> {
        match self.inner.as_ref() {
            TransportInner::Smtp(t) => {
                t.test_connection().await?;
            }
            TransportInner::Blackhole | TransportInner::Sendmail(_) => {}
        }
        Ok(())
    }
}
impl Default for TransportInner {
    fn default() -> Self {
        Self::Blackhole
    }
}
#[derive(Debug, Error)]
#[error(transparent)]
pub enum Error {
    Smtp(#[from] lettre::transport::smtp::Error),
    Sendmail(#[from] lettre::transport::sendmail::Error),
}
#[async_trait]
impl AsyncTransport for Transport {
    type Ok = ();
    type Error = Error;
    async fn send_raw(&self, envelope: &Envelope, email: &[u8]) -> Result<Self::Ok, Self::Error> {
        match self.inner.as_ref() {
            TransportInner::Blackhole => {
                tracing::warn!(
                    "An email was supposed to be sent but no email backend is configured"
                );
            }
            TransportInner::Smtp(t) => {
                t.send_raw(envelope, email).await?;
            }
            TransportInner::Sendmail(t) => {
                t.send_raw(envelope, email).await?;
            }
        };
        Ok(())
    }
}