use std::time::Duration;

use config::AppConfig;
use lettre::{
    Message, Transport,
    message::{Mailbox, header::ContentType},
    transport::smtp::{PoolConfig, SmtpTransport, authentication::Credentials},
};
use tracing::warn;

use crate::EmailMessage;

#[derive(Clone)]
pub struct SmtpEmailSender {
    mailer: SmtpTransport,
    from: Mailbox,
    retry_attempts: u32,
    retry_base_delay: Duration,
}

impl SmtpEmailSender {
    pub fn new(config: &AppConfig) -> anyhow::Result<Self> {
        let smtp_host = config.smtp_host()?;
        let smtp_port = config.smtp_port()?;
        let smtp_username = config.smtp_username()?;
        let smtp_password = config.smtp_password()?;
        let smtp_from = config.smtp_from()?;
        let smtp_tls = config.smtp_tls()?;
        let smtp_timeout = config.smtp_timeout()?;

        let builder = if smtp_tls {
            if smtp_port == 465 {
                SmtpTransport::relay(&smtp_host)?
            } else {
                SmtpTransport::starttls_relay(&smtp_host)?
            }
        } else {
            SmtpTransport::builder_dangerous(&smtp_host)
        };

        let mailer = builder
            .credentials(Credentials::new(smtp_username, smtp_password))
            .port(smtp_port)
            .timeout(Some(Duration::from_secs(smtp_timeout)))
            .pool_config(PoolConfig::new().min_idle(0).max_size(10))
            .build();

        Ok(Self {
            mailer,
            from: smtp_from.parse()?,
            retry_attempts: config.email_send_retry_attempts(),
            retry_base_delay: Duration::from_secs(
                config.email_send_retry_base_delay_secs(),
            ),
        })
    }

    pub async fn send(&self, message: EmailMessage) -> anyhow::Result<()> {
        let recipient: Mailbox = message.to.parse()?;
        let email = Message::builder()
            .from(self.from.clone())
            .to(recipient)
            .subject(message.subject)
            .header(ContentType::TEXT_PLAIN)
            .body(message.body)?;

        let attempts = self.retry_attempts.max(1);
        let mut last_error = None;

        for attempt in 0..attempts {
            let mailer = self.mailer.clone();
            let email = email.clone();
            let result =
                tokio::task::spawn_blocking(move || mailer.send(&email)).await;

            match result {
                Ok(Ok(_)) => return Ok(()),
                Ok(Err(error)) => {
                    last_error = Some(anyhow::anyhow!(error));
                    warn!(attempt = attempt + 1, "email send attempt failed");
                }
                Err(error) => {
                    return Err(anyhow::anyhow!(
                        "email send task failed: {error}"
                    ));
                }
            }

            if attempt + 1 < attempts {
                let multiplier =
                    1u64.checked_shl(attempt).unwrap_or(u64::MAX).max(1);
                tokio::time::sleep(
                    self.retry_base_delay.saturating_mul(multiplier as u32),
                )
                .await;
            }
        }

        Err(last_error.unwrap_or_else(|| anyhow::anyhow!("email send failed")))
    }
}

#[cfg(test)]
mod tests {
    use std::collections::HashMap;

    use config::AppConfig;

    use super::SmtpEmailSender;

    #[test]
    fn smtp_config_accepts_credentials_with_url_special_characters() {
        let config = AppConfig {
            env: HashMap::from([
                ("APP_SMTP_HOST".to_string(), "smtp.example.com".to_string()),
                ("APP_SMTP_PORT".to_string(), "587".to_string()),
                (
                    "APP_SMTP_USERNAME".to_string(),
                    "user@example.com".to_string(),
                ),
                (
                    "APP_SMTP_PASSWORD".to_string(),
                    "p@ss:word/with?chars".to_string(),
                ),
                (
                    "APP_SMTP_FROM".to_string(),
                    "Gitdata <noreply@example.com>".to_string(),
                ),
                ("APP_SMTP_TLS".to_string(), "true".to_string()),
            ]),
        };

        assert!(SmtpEmailSender::new(&config).is_ok());
    }
}
