use anyhow::{Context, Result};
use axum::extract::FromRef;
use lettre::{AsyncSmtpTransport, AsyncTransport, Tokio1Executor};
use serde::{Serialize, Serializer};

use crate::app_state::AppState;

const POSTMARK_EMAIL_BATCH_URL: &'static str = "https://api.postmarkapp.com/email/batch";

#[derive(Serialize)]
pub struct Message {
    #[serde(rename = "From")]
    pub from: lettre::message::Mailbox,
    #[serde(rename = "To")]
    #[serde(serialize_with = "serialize_mailboxes")]
    pub to: lettre::message::Mailboxes,
    #[serde(rename = "Subject")]
    pub subject: String,
    #[serde(rename = "TextBody")]
    pub text_body: String,
}

pub trait MailSender: Clone + Sync {
    async fn send_batch(&self, emails: Vec<Message>) -> Result<(), anyhow::Error>;
}

#[derive(Clone, Debug)]
pub enum Mailer {
    Smtp(SmtpSender),
    Postmark(PostmarkSender),
}

impl Mailer {
    pub fn new_smtp(opts: SmtpOptions) -> Result<Self> {
        Ok(Self::Smtp(SmtpSender::new(opts)?))
    }

    pub fn new_postmark(server_token: String) -> Result<Self> {
        Ok(Self::Postmark(PostmarkSender::new(server_token)?))
    }

    pub fn with_reqwest_client(self, client: reqwest::Client) -> Self {
        match self {
            Self::Postmark(sender) => Self::Postmark(sender.with_reqwest_client(client)),
            _ => self,
        }
    }
}

impl MailSender for Mailer {
    async fn send_batch(&self, emails: Vec<Message>) -> Result<(), anyhow::Error> {
        match self {
            Mailer::Smtp(sender) => sender.send_batch(emails).await,
            Mailer::Postmark(sender) => sender.send_batch(emails).await,
        }
    }
}

#[derive(Clone, Debug)]
struct SmtpSender {
    transport: AsyncSmtpTransport<Tokio1Executor>,
}

#[derive(Debug)]
pub struct SmtpOptions {
    pub server: String,
    pub username: String,
    pub password: String,
}

impl SmtpSender {
    fn new(opts: SmtpOptions) -> Result<Self> {
        let mailer_creds =
            lettre::transport::smtp::authentication::Credentials::new(opts.username, opts.password);
        let transport = lettre::AsyncSmtpTransport::<Tokio1Executor>::starttls_relay(&opts.server)
            .context("unable to initialize starttls_relay")?
            .credentials(mailer_creds)
            .build();
        Ok(Self { transport })
    }
}

impl TryInto<lettre::Message> for Message {
    type Error = anyhow::Error;

    fn try_into(self) -> Result<lettre::Message, Self::Error> {
        let mut builder = lettre::Message::builder()
            .from(self.from.clone())
            .subject(self.subject)
            .header(lettre::message::header::ContentType::TEXT_PLAIN);
        for to_addr in self.to {
            builder = builder.to(to_addr);
        }
        let message = builder.body(self.text_body)?;
        Ok(message)
    }
}

fn serialize_mailboxes<S>(t: &lettre::message::Mailboxes, s: S) -> Result<S::Ok, S::Error>
where
    S: Serializer,
{
    Ok(s.serialize_str(&t.to_string())?)
}

impl MailSender for SmtpSender {
    async fn send_batch(&self, emails: Vec<Message>) -> Result<()> {
        for email in emails {
            self.transport.send(email.try_into()?).await?;
        }
        Ok(())
    }
}

#[derive(Clone, Debug)]
struct PostmarkSender {
    client: reqwest::Client,
    server_token: String,
}

impl PostmarkSender {
    fn new(server_token: String) -> Result<Self> {
        Ok(Self {
            client: reqwest::ClientBuilder::new().https_only(true).build()?,
            server_token,
        })
    }

    pub fn with_reqwest_client(self, client: reqwest::Client) -> Self {
        Self { client, ..self }
    }
}

impl MailSender for PostmarkSender {
    async fn send_batch(&self, emails: Vec<Message>) -> Result<()> {
        if emails.len() > 500 {
            return Err(anyhow::anyhow!(
                "Postmark sends no more than 500 messages per batch"
            ));
        }
        self.client
            .post(POSTMARK_EMAIL_BATCH_URL)
            .header("X-Postmark-Server-Token", &self.server_token)
            .json(&emails)
            .send()
            .await?;
        Ok(())
    }
}

impl FromRef<AppState> for Mailer {
    fn from_ref(state: &AppState) -> Mailer {
        state.mailer.clone()
    }
}
