use std::path::{Path, PathBuf};
use anyhow::anyhow;
use crate::counters;
use lettre::message::{Mailbox, MultiPart, SinglePart, header};
use lettre::{FileTransport, SendmailTransport, SmtpTransport, Transport as LettreTransport};
use rocket_dyn_templates::handlebars::Handlebars;
use serde::Serialize;
use uuid::Uuid;

use gettext_macros::i18n;
use rocket_i18n::I18n;

use crate::template_helpers;

use crate::Result;
use hagrid_database::types::Email;

mod context {
    use serde_derive::Serialize;

    #[derive(Serialize, Clone)]
    pub struct Verification {
        pub lang: String,
        pub primary_fp: String,
        pub uri: String,
        pub userid: String,
        pub base_uri: String,
        pub domain: String,
    }

    #[derive(Serialize, Clone)]
    pub struct Manage {
        pub lang: String,
        pub primary_fp: String,
        pub uri: String,
        pub base_uri: String,
        pub domain: String,
    }

    #[derive(Serialize, Clone)]
    pub struct Welcome {
        pub lang: String,
        pub primary_fp: String,
        pub uri: String,
        pub base_uri: String,
        pub domain: String,
    }
}

pub struct Service {
    from: Mailbox,
    domain: String,
    templates: Handlebars<'static>,
    transport: Transport,
}

enum Transport {
    LocalSmtp,
    Sendmail,
    Filemail(PathBuf),
}

impl Service {
    /// Sends mail via sendmail.
    pub fn sendmail(from: &str, base_uri: &str, template_dir: &Path) -> Result<Self> {
        Self::new(from, base_uri, template_dir, Transport::Sendmail)
    }

    /// Sends mail via local smtp server.
    pub fn localsmtp(from: &str, base_uri: &str, template_dir: &Path) -> Result<Self> {
        Self::new(from, base_uri, template_dir, Transport::LocalSmtp)
    }

    /// Sends mail by storing it in the given directory.
    pub fn filemail(from: &str, base_uri: &str, template_dir: &Path, path: &Path) -> Result<Self> {
        Self::new(
            from,
            base_uri,
            template_dir,
            Transport::Filemail(path.to_owned()),
        )
    }

    fn new(from: &str, base_uri: &str, template_dir: &Path, transport: Transport) -> Result<Self> {
        let templates = template_helpers::load_handlebars(template_dir)?;
        let domain = url::Url::parse(base_uri)?
            .host_str()
            .ok_or_else(|| anyhow!("No host in base-URI"))?
            .to_string();
        let from = from
            .parse()
            .map_err(|_| anyhow!("From must be valid email address"))?;
        Ok(Self {
            from,
            domain,
            templates,
            transport,
        })
    }

    pub fn send_verification(
        &self,
        i18n: &I18n,
        base_uri: &str,
        tpk_name: String,
        userid: &Email,
        token: &str,
    ) -> Result<()> {
        let ctx = context::Verification {
            lang: i18n.lang.to_string(),
            primary_fp: tpk_name,
            uri: format!("{}/verify/{}", base_uri, token),
            userid: userid.to_string(),
            base_uri: base_uri.to_owned(),
            domain: self.domain.clone(),
        };

        counters::inc_mail_sent("verify", userid);

        self.send(
            &[userid],
            &i18n!(
                i18n.catalog,
                context = "Subject for verification email, {0} = userid, {1} = keyserver domain",
                "Verify {0} for your key on {1}";
                userid,
                self.domain.as_str(),
            ),
            "verify",
            i18n.lang,
            ctx,
        )
    }

    pub fn send_manage_token(
        &self,
        i18n: &I18n,
        base_uri: &str,
        tpk_name: String,
        recipient: &Email,
        link_path: &str,
    ) -> Result<()> {
        let ctx = context::Manage {
            lang: i18n.lang.to_string(),
            primary_fp: tpk_name,
            uri: format!("{}{}", base_uri, link_path),
            base_uri: base_uri.to_owned(),
            domain: self.domain.clone(),
        };

        counters::inc_mail_sent("manage", recipient);

        self.send(
            &[recipient],
            &i18n!(
                i18n.catalog,
                context = "Subject for manage email, {} = keyserver domain",
                "Manage your key on {}";
                self.domain.as_str()
            ),
            "manage",
            i18n.lang,
            ctx,
        )
    }

    pub fn send_welcome(
        &self,
        base_uri: &str,
        tpk_name: String,
        userid: &Email,
        token: &str,
    ) -> Result<()> {
        let ctx = context::Welcome {
            lang: "en".to_owned(),
            primary_fp: tpk_name,
            uri: format!("{}/upload/{}", base_uri, token),
            base_uri: base_uri.to_owned(),
            domain: self.domain.clone(),
        };

        counters::inc_mail_sent("welcome", userid);

        self.send(
            &[userid],
            &format!("Your key upload on {domain}", domain = self.domain),
            "welcome",
            "en",
            ctx,
        )
    }

    fn render_template(
        &self,
        template: &str,
        locale: &str,
        ctx: impl Serialize,
    ) -> Result<(String, String)> {
        let html = self
            .templates
            .render(&format!("{}/{}.htm", locale, template), &ctx)
            .or_else(|_| self.templates.render(&format!("{}.htm", template), &ctx))
            .map_err(|_| anyhow!("Email template failed to render"))?;
        let txt = self
            .templates
            .render(&format!("{}/{}.txt", locale, template), &ctx)
            .or_else(|_| self.templates.render(&format!("{}.txt", template), &ctx))
            .map_err(|_| anyhow!("Email template failed to render"))?;

        Ok((html, txt))
    }

    fn send(
        &self,
        tos: &[&Email],
        subject: &str,
        template: &str,
        locale: &str,
        ctx: impl Serialize,
    ) -> Result<()> {
        let (html, txt) = self.render_template(template, locale, ctx)?;

        if cfg!(debug_assertions) {
            for recipient in tos.iter() {
                println!("To: {}", recipient);
            }
            println!("{}", &txt);
        }

        let mut email = lettre::Message::builder()
            .from(self.from.clone())
            .subject(subject)
            .message_id(Some(format!("<{}@{}>", Uuid::new_v4(), self.domain)))
            .header(header::ContentTransferEncoding::EightBit);

        for to in tos.iter() {
            email = email.to(to.as_str().parse().unwrap());
        }

        let email = email.multipart(
            MultiPart::alternative()
                .singlepart(
                    SinglePart::builder()
                        .header(header::ContentTransferEncoding::EightBit)
                        .header(header::ContentType::TEXT_PLAIN)
                        .body(txt),
                )
                .singlepart(
                    SinglePart::builder()
                        .header(header::ContentTransferEncoding::EightBit)
                        .header(header::ContentType::TEXT_HTML)
                        .body(html),
                ),
        )?;

        match self.transport {
            Transport::LocalSmtp => {
                let transport = SmtpTransport::unencrypted_localhost();
                transport.send(&email)?;
            }
            Transport::Sendmail => {
                let transport = SendmailTransport::new();
                transport.send(&email)?;
            }
            Transport::Filemail(ref path) => {
                let transport = FileTransport::new(path);
                transport.send(&email)?;
            }
        }

        Ok(())
    }
}

/// Returns and removes the first mail it finds from the given
/// directory.
#[cfg(test)]
pub fn pop_mail(dir: &Path) -> Result<Option<String>> {
    use std::{fs, fs::read_to_string};
    for entry in fs::read_dir(dir)? {
        let entry = entry?;
        if entry.file_type()?.is_file() {
            let body = read_to_string(entry.path())?.replace("\r\n", "\n");
            fs::remove_file(entry.path())?;
            println!("{}", body);
            return Ok(Some(body));
        }
    }
    Ok(None)
}

#[cfg(test)]
mod test {
    use crate::web::get_i18n;

    use super::*;
    use std::str::FromStr;
    use tempfile::{TempDir, tempdir};

    const BASEDIR: &str = "http://localhost/";
    const FROM: &str = "test@localhost";
    const TO: &str = "recipient@example.org";

    fn configure_i18n(lang: &'static str) -> I18n {
        let langs = get_i18n();
        let catalog = langs
            .clone()
            .into_iter()
            .find(|(l, _)| *l == lang)
            .unwrap()
            .1;
        rocket_i18n::I18n { catalog, lang }
    }

    fn configure_mail() -> (Service, TempDir) {
        let template_dir: PathBuf = ::std::env::current_dir()
            .unwrap()
            .join("dist/email-templates")
            .to_str()
            .unwrap()
            .into();
        let tempdir = tempdir().unwrap();
        let service = Service::filemail(FROM, BASEDIR, &template_dir, tempdir.path()).unwrap();
        (service, tempdir)
    }

    fn assert_header(headers: &[(&str, &str)], name: &str, pred: impl Fn(&str) -> bool) {
        if let Some((_, v)) = headers.iter().find(|(h, _)| *h == name) {
            assert!(pred(v));
        } else {
            panic!("Missing header: {}", name);
        }
    }

    fn check_headers(mail_content: &str) {
        // this naively assumes that all lines colons are headers, and that all headers fit in
        // a single line. that's not accurate, but ok for our testing.
        let headers: Vec<_> = mail_content
            .lines()
            .filter(|line| line.contains(": "))
            .map(|line| line.split_once(": ").unwrap())
            .collect();
        assert!(headers.contains(&("Content-Type", "text/plain; charset=utf-8")));
        assert!(headers.contains(&("Content-Type", "text/html; charset=utf-8")));
        assert!(headers.contains(&("From", "test@localhost")));
        assert!(headers.contains(&("To", "recipient@example.org")));
        assert_header(&headers, "Content-Type", |v| {
            v.starts_with("multipart/alternative")
        });
        assert_header(&headers, "Date", |v| v.contains("-0000"));
        assert_header(&headers, "Message-ID", |v| v.contains("@localhost>"));
    }

    #[test]
    fn pop_mail_empty() {
        let (_mail, tempdir) = configure_mail();
        assert!(pop_mail(tempdir.path()).unwrap().is_none());
    }

    #[test]
    fn check_verification_mail_en() {
        let (mail, tempdir) = configure_mail();
        let i18n = configure_i18n("en");
        let recipient = Email::from_str(TO).unwrap();

        mail.send_verification(
            &i18n,
            "test",
            "fingerprintoo".to_owned(),
            &recipient,
            "token",
        )
        .unwrap();
        let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();

        check_headers(&mail_content);
        assert!(mail_content.contains("lang=\"en\""));
        assert!(mail_content.contains("Hi,"));
        assert!(mail_content.contains("fingerprintoo"));
        assert!(mail_content.contains("test/verify/token"));
        assert!(mail_content.contains("test/about"));
        assert!(mail_content.contains("To let others find this key"));
    }

    #[test]
    fn check_verification_mail_ja() {
        let (mail, tempdir) = configure_mail();
        let i18n = configure_i18n("ja");
        let recipient = Email::from_str(TO).unwrap();

        mail.send_verification(
            &i18n,
            "test",
            "fingerprintoo".to_owned(),
            &recipient,
            "token",
        )
        .unwrap();
        let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();

        check_headers(&mail_content);
        assert!(mail_content.contains("lang=\"ja\""));
        assert!(mail_content.contains("どうも、"));
        assert!(mail_content.contains("fingerprintoo"));
        assert!(mail_content.contains("test/verify/token"));
        assert!(mail_content.contains("test/about"));
        assert!(mail_content.contains("あなたのメールアド"));
        assert!(mail_content.contains(
            "Subject:   =?utf-8?b?bG9jYWxob3N044Gu44GC44Gq44Gf44Gu6Y2144Gu44Gf44KB44GrbG9jYWxob3N044KS5qSc6Ki844GZ44KL?="
        ));
    }

    #[test]
    fn check_manage_mail_en() {
        let (mail, tempdir) = configure_mail();
        let i18n = configure_i18n("en");
        let recipient = Email::from_str(TO).unwrap();

        mail.send_manage_token(
            &i18n,
            "test",
            "fingerprintoo".to_owned(),
            &recipient,
            "token",
        )
        .unwrap();
        let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();

        check_headers(&mail_content);
        assert!(mail_content.contains("lang=\"en\""));
        assert!(mail_content.contains("Hi,"));
        assert!(mail_content.contains("fingerprintoo"));
        assert!(mail_content.contains("testtoken"));
        assert!(mail_content.contains("test/about"));
        assert!(mail_content.contains("manage and delete"));
    }

    #[test]
    fn check_manage_mail_ja() {
        let (mail, tempdir) = configure_mail();
        let i18n = configure_i18n("ja");
        let recipient = Email::from_str(TO).unwrap();

        mail.send_manage_token(
            &i18n,
            "test",
            "fingerprintoo".to_owned(),
            &recipient,
            "token",
        )
        .unwrap();
        let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();

        check_headers(&mail_content);
        print!("{}", mail_content);
        assert!(mail_content.contains("lang=\"ja\""));
        assert!(mail_content.contains("どうも、"));
        assert!(mail_content.contains("fingerprintoo"));
        assert!(mail_content.contains("testtoken"));
        assert!(mail_content.contains("test/about"));
        assert!(mail_content.contains("この鍵の掲示されたア"));
        assert!(
            mail_content.contains("Subject: =?utf-8?b?bG9jYWxob3N044Gu6Y2144KS566h55CG44GZ44KL?=")
        );
    }

    #[test]
    fn check_welcome_mail() {
        let (mail, tempdir) = configure_mail();
        let recipient = Email::from_str(TO).unwrap();

        mail.send_welcome("test", "fingerprintoo".to_owned(), &recipient, "token")
            .unwrap();
        let mail_content = pop_mail(tempdir.path()).unwrap().unwrap();

        check_headers(&mail_content);
        assert!(mail_content.contains("lang=\"en\""));
        assert!(mail_content.contains("Hi,"));
        assert!(mail_content.contains("fingerprintoo"));
        assert!(mail_content.contains("test/upload/token"));
        assert!(mail_content.contains("test/about"));
        assert!(mail_content.contains("first time"));
    }
}
