#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use] extern crate rocket;
#[macro_use] extern crate serde_derive;
extern crate rocket_contrib;
extern crate lettre;
extern crate lettre_email;
extern crate regex;
extern crate urlencoding;
extern crate config;
extern crate strfmt;
extern crate rocket_client_addr;
use rocket_client_addr::ClientRealAddr;

use strfmt::strfmt;

use rocket::Request;
use rocket::State;
use rocket::outcome::Outcome;
use rocket_contrib::serve::StaticFiles;
use rocket::response::Redirect;
use rocket_contrib::templates::Template;
use rocket::request::{Form, FromRequest};

use lettre::smtp::authentication::{Credentials, Mechanism};
use lettre_email::{Email, mime::TEXT_PLAIN};
use lettre::{Transport, SmtpClient};
use lettre::smtp::extension::ClientId;

use std::collections::HashMap;
use std::sync::Mutex;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};

use regex::Regex;

pub mod settings;
use settings::{Settings, LanguageStrings};

//////////////////////////
// Structs
//////////////////////////
#[derive(FromForm)]
struct OwnedUserInput {
    name: String,
    email: String,
    guests: u8,
    message: String
}

pub struct HostHeader<'a>(pub &'a str);
impl<'a, 'r> FromRequest<'a, 'r> for HostHeader<'a> {
    type Error = ();

    fn from_request(request: &'a Request) -> rocket::request::Outcome<Self, Self::Error> {
        match request.headers().get_one("Host") {
            Some(h) => Outcome::Success(HostHeader(h)),
            None => Outcome::Forward(()),
        }
    }
}

//////////////////////////
// Functions
//////////////////////////
fn lang_strings<'a>(host: HostHeader, settings: &'a Settings) -> &'a LanguageStrings {
    // Grab TLD and grab the mapping from the settings. Check with a match construct
    // whether there is a TLD (corner case: running the site in development without
    // a valid TLD).
    let lang = match strip_tld(&host.0) {
        Some(tld) => {
            // If the TLD is valid, try to get the mapping to a language
            let tld_mapping = settings.tld_mapping.get(&tld);

            // See if this is a valid mapping, otherwise, fall back to the default
            let lang = match tld_mapping {
                    Some(lang) => lang,
                    None => &settings.default_lang,
            };

            // Return to variable
            lang

        },
        // Return default language
        None => &settings.default_lang
    };

    // Grab actual language strings and return
    &settings.languages.get(lang).unwrap()
}

fn validate_name(name: &String) -> bool {
    // Check if the name only consists of letters and numbers
    // This is a placeholder and currently simply returns true
    if name.len() > 0 {
        true
    } else {
        false
    }
}

fn validate_email(email: &String) -> bool {
    // Check if the emailaddress is valid
    Regex::new(r"^[a-zA-Z0-9\.\-]+@[a-zA-Z0-9\-]+\.[a-zA-Z]+$").unwrap().is_match(email)
}

fn strip_tld(url: &str) -> Option<String> {
    // Get the top level domain which is used to determine the language

    let re = Regex::new(r"^.*\.([a-z]+)$").unwrap();

    let tld = match re.captures(url) {
        Some(tld) => Some(tld[1].to_string()),
        None => None
    };

    tld
}


fn send_email(to_name: &String, to_address: &String, 
              subject: &String, message: &String, 
              attachment: Option<&String>, settings: &State<Settings>) {
    // Load HJSON config
    let email_credentials = &settings.email_credentials;

    // Translate config into more readable variables
    let smtp_server = email_credentials.smtp_server.to_string();
    let smtp_name = email_credentials.smtp_name.to_string();
    let smtp_username = email_credentials.smtp_username.to_string();
    let smtp_password = email_credentials.smtp_password.to_string();

    // If an attachment is defined, send it.
    let email = match attachment {
        Some(x) => {
            Email::builder()
                .to((to_address, to_name))
                .from((&smtp_username, &smtp_name))
                .subject(subject)
                .html(message)
                .attachment_from_file(Path::new(x), None, &TEXT_PLAIN)
                .unwrap()
                .build()
                .unwrap()
        },
        None => {
            Email::builder()
                .to((to_address, to_name))
                .from((&smtp_username, &smtp_name))
                .subject(subject)
                .html(message)
                .build()
                .unwrap()
        }
    };
    
    // Connect to a remote server on a custom port
    let mut mailer = SmtpClient::new_simple(&smtp_server).unwrap()
        // Set the name sent during EHLO/HELO, default is `localhost`
        .hello_name(ClientId::Domain(smtp_server))
        // Add credentials for authentication
        .credentials(Credentials::new(smtp_username, smtp_password))
        // Enable SMTPUTF8 if the server supports it
        .smtp_utf8(true)
        // Configure expected authentication mechanism
        .authentication_mechanism(Mechanism::Plain).transport();
    
    let result = mailer.send(email.into());
    assert!(result.is_ok());
    
    mailer.close();
}

//////////////////////////
// Pages of website
//////////////////////////
#[post("/", data = "<user_input>")]
fn submit_task (host: HostHeader, 
                settings: State<Settings>, 
                user_input: Option<Form<OwnedUserInput>>,
                client_addr: &ClientRealAddr, 
                ip_epoch: State<Mutex<HashMap<String, u64>>>) -> Redirect { 
    // Create struct with all language strings 
    let language_strings = lang_strings(host, &settings);

    match user_input {
        Some(x) => {
            // Check input for anything strange or something that does not look like
            // an email address.
            if validate_name(&x.name) && validate_email(&x.email) {
                // Create HashMap to use with strfnt
                let mut vars = HashMap::new();
                vars.insert("name".to_string(), &x.name);

                // Check hashtable to make sure this IP doesn't try to spam.
                let epoch = SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .expect("Time went backwards")
                    .as_secs();

                // Unlock mutex on ip->epoch HashTable
                let mut ip_epoch_unlocked = ip_epoch.inner().lock().unwrap();

                // Check how long ago the latest message was sent
                match ip_epoch_unlocked.get(&client_addr.get_ipv4_string().unwrap()) {
                    Some(x) => {
                        if epoch - x < settings.spam_wait {
                            return Redirect::to("spam");
                        } else {
                            // We are OK to send!
                            ip_epoch_unlocked.insert(client_addr.get_ipv4_string().unwrap(), epoch);
                        }

                    }
                    None => {
                        // We are OK to send!
                        ip_epoch_unlocked.insert(client_addr.get_ipv4_string().unwrap(), epoch);
                    }
                };

                send_email(&x.name, 
                           &x.email, 
                           &language_strings.email.subject,
                           &strfmt(&language_strings.email.message, &vars).unwrap(),
                           Some(&language_strings.email.attachment),
                           &settings);
            }
            else {
                return Redirect::to("404");
            }

            // Send a message to the sender's e-mailaddres to inform about
            // a new attendent.
            let email_credentials = &settings.email_credentials;
            let smtp_name = email_credentials.smtp_name.to_string();
            let smtp_username = email_credentials.smtp_username.to_string();
            let mut rsvp_subject = String::new();
            let mut rsvp_message = String::new();

            rsvp_subject.push_str(&x.name);
            rsvp_subject.push_str(" just responded to your event!");

            rsvp_message.push_str("Name: ");
            rsvp_message.push_str(&x.name);
            rsvp_message.push_str("<br />Emailaddress: ");
            rsvp_message.push_str(&x.email);
            rsvp_message.push_str("<br />Guests: ");
            rsvp_message.push_str(&x.guests.to_string());
            rsvp_message.push_str("<br />Message:<br /><br />");
            rsvp_message.push_str(&x.message);

            send_email(&smtp_name, &smtp_username, &rsvp_subject, &rsvp_message, None, &settings);
            Redirect::to(format!("/thanks/{}", urlencoding::encode(&x.name)))
        }
        None => Redirect::to("404")
    
    }
}

#[get("/thanks/<name>")]
fn thanks(name: String, host:HostHeader, settings: State<Settings>) -> Template {
    // Create struct with all language strings
    let language_strings = lang_strings(host, &settings).clone();

    // Create HashMap to use with strfnt
    let mut vars = HashMap::new();
    vars.insert("name".to_string(), name);

    // Create template with appropriate strings
    Template::render("message", 
        &settings::MessageStrings{title : language_strings.thanks.title.clone(),
                                  h1 : strfmt(&language_strings.thanks.h1, &vars).unwrap(),
                                  message : language_strings.thanks.message.clone()})
}

#[get("/spam")]
fn spam(host:HostHeader, settings: State<Settings>) -> Template {
    // Create struct with all language strings
    let language_strings = lang_strings(host, &settings).clone();

    // Create template with appropriate strings
    Template::render("message", &language_strings.spam)
}

#[get("/")]
fn index(host: HostHeader, settings: State<Settings>) -> Template {
    // Create struct with all language strings
    let language_strings = lang_strings(host, &settings);

    // Parse index page
    Template::render("index", &language_strings.form)
}

#[catch(404)]
fn not_found() -> Template {
    let h1 = String::from("Oops..! 404 :-(");
    let title = "404".to_string();
    let message = "".to_string();
    let context = settings::MessageStrings{title, h1, message};    
    Template::render("message", &context)  
}


//////////////////////////
// Launch Rocket
//////////////////////////
fn main() {
    let ip_epoch: Mutex<HashMap<String, u64>> = Mutex::new(HashMap::new());

    rocket::ignite()
		.manage(Settings::new().unwrap())
        .manage(ip_epoch)
        .mount("/", routes![index])
        .mount("/", routes![thanks])
        .mount("/", routes![spam])
        .mount("/", routes![submit_task])
        .mount("/css", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/css")))
        .mount("/js", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/js")))
        .mount("/vendor", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/vendor")))
        .mount("/images", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/images")))
        .mount("/fonts", StaticFiles::from(concat!(env!("CARGO_MANIFEST_DIR"), "/templates/fonts")))
        .attach(Template::fairing())
        .register(catchers![not_found])
        .launch();
}
