use crate::auth::{self, AuthData, ExpectedCaptcha};
use crate::StaticDir;
use crate::{Mailer, PgDevandConn};
use devand_crypto::{EmailVerification, PasswordReset, Signable, SignedToken};
use devand_text::Text;
use rocket::http::uri::Uri;
use rocket::http::{ContentType, Cookies};
use rocket::request::{FlashMessage, Form};
use rocket::response::{Content, Flash, NamedFile, Redirect};
use rocket::{Route, State};
use rocket_contrib::templates::Template;
use serde::Serialize;

const BASE_URL: Option<&'static str> = option_env!("DEVAND_BASE_URL");
const DEFAULT_BASE_URL: &str = "http://localhost:8000";

// Handle authentication request
#[post("/login/<return_to>", data = "<credentials>")]
fn login(
    return_to: String,
    mut cookies: Cookies,
    credentials: Form<auth::Credentials>,
    real_ip: auth::RealIp,
    conn: PgDevandConn,
) -> Result<Redirect, Flash<Redirect>> {
    auth::login(&mut cookies, credentials.0, &conn)
        .map(|_| Redirect::to(return_to.clone()))
        .map_err(|_| {
            log_fail(real_ip.0);
            Flash::error(
                Redirect::to(uri!(login_page: return_to)),
                Text::LoginError.to_string(),
            )
        })
}

#[get("/login")]
fn login_page_default() -> Redirect {
    Redirect::to(uri!(login_page: "/".to_string()))
}

#[get("/login/<return_to>")]
fn login_page(
    return_to: String,
    auth_data: Option<auth::AuthData>,
    flash: Option<FlashMessage>,
) -> Result<Template, Redirect> {
    if auth_data.is_some() {
        // When user is authenticated, /login just redirect to index
        Err(Redirect::to(uri!(index)))
    } else {
        // When user is not authenticated, /login displays a form
        #[derive(Serialize)]
        struct Context {
            title: &'static str,
            flash_msg: Option<String>,
            flash_name: Option<String>,
            authenticated: bool,
            return_to: String,
        }

        let context = Context {
            title: "Sign in to DevAndDev",
            flash_msg: flash.as_ref().map(|x| x.msg().to_string()),
            flash_name: flash.as_ref().map(|x| x.name().to_string()),
            authenticated: false,
            return_to: Uri::percent_encode(&return_to).to_string(),
        };

        Ok(Template::render("login", &context))
    }
}

/// /logout just remove the cookie
#[post("/logout")]
fn logout(mut cookies: Cookies) -> Flash<Redirect> {
    auth::logout(&mut cookies);
    Flash::success(
        Redirect::to(uri!(login_page: String::new())),
        Text::LogoutDone.to_string(),
    )
}

#[get("/password_reset")]
fn password_reset_page(flash: Option<FlashMessage>) -> Template {
    #[derive(Serialize)]
    struct Context {
        title: String,
        flash_msg: Option<String>,
        flash_name: Option<String>,
        authenticated: bool,
    }

    let title = Text::ResetYourPassowrd.to_string();
    let context = Context {
        title,
        flash_msg: flash.as_ref().map(|x| x.msg().to_string()),
        flash_name: flash.as_ref().map(|x| x.name().to_string()),
        authenticated: false,
    };

    Template::render("password_reset", &context)
}

#[derive(FromForm)]
pub struct PasswordReset1 {
    email: String,
}

#[post("/password_reset", data = "<password_reset>")]
fn password_reset(
    password_reset: Form<PasswordReset1>,
    real_ip: auth::RealIp,
    mailer: State<Mailer>,
    crypto_encoder: State<devand_crypto::Encoder>,
    conn: PgDevandConn,
) -> Result<Redirect, Flash<Redirect>> {
    let err_msg = Text::EmailNotAssociatedToAccount.to_string();
    let PasswordReset1 { email } = password_reset.0;

    if let Some(user) = devand_db::load_user_by_email(email.as_str(), &conn) {
        let user_id = user.id;
        let data = PasswordReset { user_id: user_id.0 };

        let token = data.sign(&crypto_encoder);

        crate::notifications::password_reset(
            BASE_URL.unwrap_or(DEFAULT_BASE_URL),
            &mailer,
            user.email,
            token,
        );

        Ok(Redirect::to(uri!(password_reset_wait_page)))
    } else {
        log_fail(real_ip.0);
        let redirect = Redirect::to(uri!(password_reset_page));
        Err(Flash::error(redirect, err_msg))
    }
}

#[get("/password_reset/<token>")]
fn password_reset_token_page(
    token: String,
    flash: Option<FlashMessage>,
    crypto_decoder: State<devand_crypto::Decoder>,
) -> Template {
    #[derive(Serialize)]
    struct Context {
        title: String,
        token: String,
        flash_msg: Option<String>,
        flash_name: Option<String>,
        authenticated: bool,
        valid_token: bool,
    }

    // Here we decode the token just to give an immediate feedback to user about its validity
    // It is checked again on form submission
    let valid_token =
        PasswordReset::try_from_token(&token.clone().into(), &crypto_decoder).is_some();

    let title = Text::ResetYourPassowrd.to_string();
    let context = Context {
        title,
        token,
        flash_msg: flash.as_ref().map(|x| x.msg().to_string()),
        flash_name: flash.as_ref().map(|x| x.name().to_string()),
        authenticated: false,
        valid_token,
    };

    Template::render("password_reset_new", &context)
}

#[get("/password_reset/wait")]
fn password_reset_wait_page() -> Template {
    #[derive(Serialize)]
    struct Context {
        title: String,
        authenticated: bool,
    }

    let title = Text::CheckYourEmail.to_string();
    let context = Context {
        title,
        authenticated: false,
    };

    Template::render("password_reset_wait", &context)
}

#[derive(FromForm)]
pub struct PasswordReset2 {
    password: String,
}

#[post("/password_reset/<token>", data = "<password_reset>")]
fn password_reset_token(
    real_ip: auth::RealIp,
    token: String,
    password_reset: Form<PasswordReset2>,
    crypto_decoder: State<devand_crypto::Decoder>,
    conn: PgDevandConn,
) -> Result<Flash<Redirect>, Flash<Redirect>> {
    let ok_msg = Text::PasswordChangedReset.to_string();
    let redirect_ok = Redirect::to(uri!(login_page: String::new()));
    let redirect_err = Redirect::to(uri!(password_reset_token_page: token.clone()));

    let PasswordReset2 { password } = password_reset.0;

    if !devand_core::auth::is_valid_password(&password) {
        let err_msg = Text::InvalidPassword.to_string();
        return Err(Flash::error(redirect_err, err_msg));
    }

    let token = SignedToken::from(token);
    let signed_data = PasswordReset::try_from_token(&token, &crypto_decoder);

    match signed_data {
        Some(PasswordReset { user_id }) => {
            let user_id = devand_core::UserId(user_id);
            devand_db::auth::set_password(user_id, &password, &conn)
                .expect("Password to be updated on database");
            Ok(Flash::success(redirect_ok, ok_msg))
        }
        None => {
            log_fail(real_ip.0);
            let err_msg = Text::InvalidToken.to_string();
            Err(Flash::error(redirect_err, err_msg))
        }
    }
}

// Handle join request
// Note: cookies must be after expected_captcha, due to One-At-A-Time cookies
// restriction
#[post("/join", data = "<join_data>")]
fn join(
    join_data: Form<auth::JoinData>,
    expected_captcha: ExpectedCaptcha,
    mut cookies: Cookies,
    real_ip: auth::RealIp,
    mailer: State<Mailer>,
    conn: PgDevandConn,
) -> Result<Redirect, Flash<Redirect>> {
    let email_address = join_data.email.clone();
    auth::join(&mut cookies, join_data.0, expected_captcha, &conn)
        .map(|_| mailer.verify_address(email_address))
        .map(|_| Redirect::to(uri!(dashboard_index)))
        .map_err(|err| {
            log_fail(real_ip.0);
            Flash::error(Redirect::to(uri!(join_page)), err.to_string())
        })
}

#[get("/join", rank = 2)]
fn join_page(
    flash: Option<FlashMessage>,
    join_data: Option<auth::JoinData>,
    auth_data: Option<AuthData>,
) -> Result<Template, Redirect> {
    if auth_data.is_some() {
        // When user is authenticated, /join just redirect to index
        Err(Redirect::to(uri!(index)))
    } else {
        // When user is not authenticated, /join displays a form
        #[derive(Serialize)]
        struct Context {
            title: String,
            flash: Option<String>,
            username: Option<String>,
            email: Option<String>,
            password: Option<String>,
            authenticated: bool,
        }

        let title = Text::CreateAccount.to_string();
        let context = Context {
            title,
            flash: flash.map(|x| x.msg().to_string()),
            username: join_data.as_ref().map(|x| x.username.to_string()),
            email: join_data.as_ref().map(|x| x.email.to_string()),
            password: join_data.as_ref().map(|x| x.password.to_string()),
            authenticated: false,
        };

        Ok(Template::render("join", &context))
    }
}

/// Generate a captcha png
#[get("/join/captcha.png")]
fn join_captcha(mut cookies: Cookies) -> Option<Content<Vec<u8>>> {
    let captcha = auth::captcha(&mut cookies).unwrap();
    let data = captcha.into_data();
    Some(Content(ContentType::PNG, data))
}

// Some URLs respond with dashboard
#[get("/dashboard")]
fn dashboard_index(auth_data: AuthData) -> Template {
    dashboard(auth_data)
}

#[get("/affinities")]
fn dashboard_affinities(auth_data: AuthData) -> Template {
    dashboard(auth_data)
}

#[get("/code-now")]
fn dashboard_code_now(auth_data: AuthData) -> Template {
    dashboard(auth_data)
}

#[get("/chat/<_username>")]
fn dashboard_chat(auth_data: AuthData, _username: String) -> Template {
    dashboard(auth_data)
}

#[get("/chat")]
fn dashboard_chats(auth_data: AuthData) -> Template {
    dashboard(auth_data)
}

#[get("/schedule")]
fn dashboard_schedule(auth_data: AuthData) -> Template {
    dashboard(auth_data)
}

#[get("/settings/password")]
fn dashboard_settings_password(auth_data: AuthData) -> Template {
    dashboard(auth_data)
}

#[get("/u/<_username>")]
fn dashboard_user_profile(auth_data: AuthData, _username: String) -> Template {
    dashboard(auth_data)
}

fn dashboard(_auth_data: AuthData) -> Template {
    #[derive(Serialize)]
    struct Context {
        title: String,
        authenticated: bool,
    }

    let title = Text::YourDashboard.to_string();
    let context = Context {
        title,
        authenticated: true,
    };

    Template::render("dashboard", &context)
}

#[get("/", rank = 2)]
fn index(auth_data: Option<AuthData>) -> Template {
    #[derive(Serialize)]
    struct Context {
        title: &'static str,
        authenticated: bool,
    }

    let context = Context {
        title: "Find your pair-programming pal",
        authenticated: auth_data.is_some(),
    };

    Template::render("index", &context)
}

#[get("/privacy")]
fn privacy(auth_data: Option<AuthData>) -> Template {
    #[derive(Serialize)]
    struct Context {
        title: &'static str,
        authenticated: bool,
    }

    let context = Context {
        title: "Privacy Policy",
        authenticated: auth_data.is_some(),
    };

    Template::render("privacy", &context)
}

#[get("/code-of-conduct")]
fn code_of_conduct(auth_data: Option<AuthData>) -> Template {
    #[derive(Serialize)]
    struct Context {
        title: String,
        authenticated: bool,
    }

    let title = Text::CodeOfConduct.to_string();
    let context = Context {
        title,
        authenticated: auth_data.is_some(),
    };

    Template::render("code-of-conduct", &context)
}

#[get("/help")]
fn help(auth_data: Option<AuthData>) -> Template {
    #[derive(Serialize)]
    struct Context {
        title: String,
        authenticated: bool,
    }

    let title = Text::NeedHelp.to_string();
    let context = Context {
        title,
        authenticated: auth_data.is_some(),
    };

    Template::render("help", &context)
}

#[get("/favicon.ico")]
fn favicon(static_dir: State<StaticDir>) -> Option<NamedFile> {
    NamedFile::open(std::path::Path::new(&static_dir.0).join("favicon.ico")).ok()
}

#[get("/verify_email/<token>")]
fn verify_email_token(
    token: String,
    auth_data: Option<AuthData>,
    crypto_decoder: State<devand_crypto::Decoder>,
) -> Template {
    // Here we decode the token just to give an immediate feedback to user about its validity
    // It is checked again on form submission
    let valid_token =
        EmailVerification::try_from_token(&token.clone().into(), &crypto_decoder).is_some();

    #[derive(Serialize)]
    struct Context {
        title: String,
        authenticated: bool,
        valid_token: bool,
        token: String,
    }

    let title = Text::EmailAddressVerification.to_string();
    let context = Context {
        title,
        authenticated: auth_data.is_some(),
        valid_token,
        token,
    };

    Template::render("email_verification_step1", &context)
}

#[post("/verify_email/<token>")]
fn verify_email_token_post(
    token: String,
    auth_data: Option<AuthData>,
    crypto_decoder: State<devand_crypto::Decoder>,
    conn: PgDevandConn,
) -> Template {
    let token = SignedToken::from(token);
    let verified_data = EmailVerification::try_from_token(&token, &crypto_decoder);

    let verified = if let Some(EmailVerification { address }) = verified_data {
        devand_db::set_verified_email(&address, &conn).expect("Email can be verified");
        true
    } else {
        false
    };

    #[derive(Serialize)]
    struct Context {
        title: String,
        authenticated: bool,
        verified: bool,
    }

    let title = Text::EmailAddressVerification.to_string();
    let context = Context {
        title,
        authenticated: auth_data.is_some(),
        verified,
    };

    Template::render("email_verification_step2", &context)
}

pub fn routes() -> Vec<Route> {
    routes![
        index,
        join,
        join_page,
        join_captcha,
        login,
        login_page_default,
        login_page,
        password_reset,
        password_reset_page,
        password_reset_token,
        password_reset_token_page,
        password_reset_wait_page,
        logout,
        dashboard_index,
        dashboard_affinities,
        dashboard_code_now,
        dashboard_schedule,
        dashboard_chat,
        dashboard_chats,
        dashboard_settings_password,
        dashboard_user_profile,
        privacy,
        code_of_conduct,
        help,
        favicon,
        verify_email_token,
        verify_email_token_post,
    ]
}

fn format_fail(ip_addr: std::net::IpAddr) -> String {
    format!("Fail: IpAddr({})", ip_addr)
}

// Used by fail2ban
fn log_fail(ip_addr: std::net::IpAddr) {
    println!("{}", format_fail(ip_addr));
}

#[cfg(test)]
mod test {
    use super::super::ignite;
    use super::*;
    use rocket::http::Status;
    use rocket::local::Client;
    use std::net::IpAddr;
    use std::net::Ipv4Addr;

    #[test]
    fn log_fail_has_expected_format() {
        assert!(format_fail(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))) == "Fail: IpAddr(1.2.3.4)");
    }

    fn make_client() -> rocket::local::Client {
        Client::new(ignite()).expect("valid rocket instance")
    }

    #[test]
    #[ignore]
    fn index_ok() {
        let client = make_client();
        let response = client.get("/").dispatch();
        assert_eq!(response.status(), Status::Ok);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn login_see_other() {
        let client = make_client();
        let response = client.get("/login").dispatch();
        assert_eq!(response.status(), Status::SeeOther);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn login_return_to_index() {
        let client = make_client();
        let response = client.get("/login/%2F").dispatch();
        assert_eq!(response.status(), Status::Ok);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn dashboard_unauthorized() {
        let client = make_client();
        let response = client.get("/dashboard").dispatch();
        assert_eq!(response.status(), Status::Unauthorized);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn password_reset_ok() {
        let client = make_client();
        let response = client.get("/password_reset").dispatch();
        assert_eq!(response.status(), Status::Ok);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn join_ok() {
        let client = make_client();
        let response = client.get("/join").dispatch();
        assert_eq!(response.status(), Status::Ok);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn join_captcha_ok() {
        let client = make_client();
        let response = client.get("/join/captcha.png").dispatch();
        assert_eq!(response.status(), Status::Ok);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn privacy_ok() {
        let client = make_client();
        let response = client.get("/privacy").dispatch();
        assert_eq!(response.status(), Status::Ok);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn code_of_conduct_ok() {
        let client = make_client();
        let response = client.get("/code-of-conduct").dispatch();
        assert_eq!(response.status(), Status::Ok);
        // TODO assert body with approval tests
    }

    #[test]
    #[ignore]
    fn help_ok() {
        let client = make_client();
        let response = client.get("/help").dispatch();
        assert_eq!(response.status(), Status::Ok);
        // TODO assert body with approval tests
    }
}
