use crate::db;
use axum::{
    debug_handler,
    extract::{FromRef, Query, State},
    response::{IntoResponse, Redirect, Response},
    Form,
};
use axum_extra::extract::cookie::{Cookie, Key, SignedCookieJar};
use cuach::*;
use diesel::{
    sql_types::Bool, BoolExpressionMethods, ExpressionMethods, OptionalExtension, QueryDsl,
};
use diesel_async::scoped_futures::ScopedFutureExt;
use diesel_async::*;
use rand::{rng, Rng};
use serde::*;
use thiserror::*;
use tracing::*;

impl FromRef<crate::Config> for Key {
    fn from_ref(state: &crate::Config) -> Self {
        Key::from(&state.hmac_secret)
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct LoginPayload {
    login: String,
    pass: String,
    redirect: Option<String>,
}

#[macro_export]
macro_rules! check_password {
    ( $p:expr ) => {{
        use diesel::sql_types::Text;
        diesel::dsl::sql::<Bool>("password = crypt(")
            .bind::<Text, _>($p)
            .sql(", password)")
    }};
}

#[macro_export]
macro_rules! make_password {
    ( $p:expr ) => {{
        use diesel::sql_types::Text;
        diesel::dsl::sql::<Text>("crypt(")
            .bind::<Text, _>($p)
            .sql(", gen_salt('bf'))")
    }};
}

fn make_cookie(id: uuid::Uuid) -> Cookie<'static> {
    let mut c = Cookie::new(
        "session_id",
        data_encoding::BASE64URL.encode(&bincode::serialize(&(id, chrono::Utc::now())).unwrap()),
    );
    c.set_path("/");
    c
}

#[debug_handler]
pub async fn login(
    State(config): State<crate::Config>,
    jar: SignedCookieJar,
    Form(payload): Form<LoginPayload>,
) -> Response {
    debug!("{:?}", payload);
    use db::users::dsl as u;
    if let Some((id, login)) = u::users
        .filter(u::email.eq(&payload.login).or(u::login.eq(&payload.login)))
        .filter(u::email_is_invalid.is_null())
        .filter(check_password!(&payload.pass))
        .select((u::id, u::login))
        .get_result::<(uuid::Uuid, String)>(&mut config.db.get().await.unwrap())
        .await
        .optional()
        .unwrap()
    {
        debug!("id {:?} {:?}", id, login);
        (
            jar.add(make_cookie(id)),
            Redirect::to(&payload.redirect.unwrap_or(format!("/{}", login))),
        )
            .into_response()
    } else {
        debug!("login not found");
        Redirect::to(payload.redirect.as_deref().unwrap_or("/")).into_response()
    }
}

#[derive(Debug, Serialize, Deserialize)]
pub struct RegisterPayload {
    login: String,
    email: String,
    pass: String,
    confpass: String,
}

#[derive(Debug, Deserialize)]
pub struct RegisterToken {
    token: String,
}

#[debug_handler]
pub async fn register_post(
    State(config): State<crate::Config>,
    Form(payload): Form<RegisterPayload>,
) -> Redirect {
    debug!("{:?}", payload);
    use db::users::dsl as u;
    let mut db = config.db.get().await.unwrap();
    if let Some(id) = diesel::insert_into(u::users)
        .values((
            u::login.eq(&payload.login),
            u::email.eq(&payload.email),
            u::password.eq(make_password!(payload.pass)),
            u::email_is_invalid.eq(true),
        ))
        .on_conflict_do_nothing()
        .returning(u::id)
        .get_result::<uuid::Uuid>(&mut db)
        .await
        .optional()
        .unwrap()
    {
        if let Err(e) = make_email(&config, &mut db, id, true, payload.email).await {
            debug!("{:?}", e);
            diesel::delete(u::users.find(id))
                .execute(&mut db)
                .await
                .unwrap();
        } else {
            return Redirect::to("/signup_mail_sent");
        }
    }
    Redirect::to("/?error=alreadyExists")
}

#[debug_handler]
pub async fn register_get(
    State(config): State<crate::Config>,
    Query(params): Query<RegisterToken>,
    jar: SignedCookieJar,
) -> Response {
    use db::tokens::dsl as tokens;
    use db::users::dsl as u;
    let mut db = config.db.get().await.unwrap();
    debug!("register_get {:?}", params);
    if let Ok(token) = data_encoding::BASE64URL.decode(params.token.as_bytes()) {
        debug!("token {:?}", token);
        if let Some(id) = diesel::delete(tokens::tokens.find(&token))
            .returning(tokens::user_id)
            .get_result::<uuid::Uuid>(&mut db)
            .await
            .optional()
            .unwrap()
        {
            diesel::update(u::users.find(id))
                .set(u::email_is_invalid.eq(None as Option<bool>))
                .execute(&mut db)
                .await
                .unwrap();
            debug!("id {:?}", id);
            if let Some(login) = u::users
                .find(id)
                .select(u::login)
                .get_result::<String>(&mut db)
                .await
                .optional()
                .unwrap()
            {
                return (
                    jar.add(make_cookie(id)),
                    Redirect::to(&format!("/{}", login)),
                )
                    .into_response();
            }
        }
    }
    Redirect::to("/").into_response()
}

#[derive(Debug, Deserialize)]
pub struct Recover {
    code: String,
    pass: String,
    pass2: String,
}

#[debug_handler]
pub async fn recover_reset(
    State(config): State<crate::Config>,
    jar: SignedCookieJar,
    Form(recover): Form<Recover>,
) -> Result<Response, crate::Error> {
    debug!("recover {:?}", recover);
    if recover.pass != recover.pass2 {
        return Ok(Redirect::to("/").into_response());
    }
    use db::tokens::dsl as tokens;
    use db::users::dsl as u;
    let mut db = config.db.get().await?;
    if let Ok(token) = data_encoding::BASE64URL.decode(recover.code.as_bytes()) {
        db.transaction(move |mut txn| {
            (async move {
                debug!("token {:?}", token);
                if let Some(id) = diesel::delete(tokens::tokens.find(&token))
                    .returning(tokens::user_id)
                    .get_result::<uuid::Uuid>(&mut txn)
                    .await
                    .optional()?
                {
                    debug!("id {:?}", id);
                    let n = diesel::update(u::users.find(id))
                        .set(u::password.eq(make_password!(recover.pass)))
                        .filter(u::email_is_invalid.is_null())
                        .returning(u::login)
                        .execute(&mut txn)
                        .await?;

                    if n > 0 {
                        return Ok((
                            jar.add(make_cookie(id)),
                            Redirect::to("/settings"),
                        )
                            .into_response());
                    } else {
                        debug!("no login");
                    }
                }
                Ok(Redirect::to("/").into_response())
            })
            .scope_boxed()
        })
        .await
    } else {
        debug!("no token decode");
        Ok(Redirect::to("/").into_response())
    }
}

#[derive(Debug, Deserialize)]
pub struct RecoverInit {
    login: String,
}

#[debug_handler]
pub async fn recover_init(
    State(config): State<crate::Config>,
    Form(recover): Form<RecoverInit>,
) -> Result<Redirect, crate::Error> {
    use db::users::dsl as u;
    let mut db = config.db.get().await.unwrap();
    if let Some((id, email)) = u::users
        .filter(u::login.eq(&recover.login).or(u::email.eq(&recover.login)))
        .select((u::id, u::email))
        .get_result::<(uuid::Uuid, String)>(&mut db)
        .await
        .optional()?
    {
        make_email(&config, &mut db, id, false, email).await?;
        Ok(Redirect::to("/recover/email_sent"))
    } else {
        Ok(Redirect::to("/recover"))
    }
}

#[derive(Error, Debug, Clone, Copy)]
pub enum Error {
    #[error("This user does not exist, or the email address used for registration was different.")]
    NoSuchUser,
    #[error("This user is inactive. Please contact <a href=\"mailto:support@pijul.org\">support@pijul.org</a> to resolve this situation.")]
    Inactive,
    #[error("The email could not be sent. Please check the address, and try again later.")]
    Email,
    #[error("Login already taken")]
    LoginAlreadyTaken,
    #[error("The login contains invalid characters")]
    InvalidLogin,
    #[error("The login is empty")]
    EmptyLogin,
    #[error("The two passwords do not match")]
    PasswordMatchError,
    #[error("Signup failed")]
    CouldNotSignup,
}

pub const CONTENT_STYLE: cuach::PreEscaped<&'static str> =
    cuach::PreEscaped("padding:1em;max-width:600px;margin:0 auto;");
pub const FOOTER_STYLE: cuach::PreEscaped<&'static str> =
    cuach::PreEscaped("max-width:600px;margin:10px auto;font-size: small; color: #666666;");
pub const A_STYLE: cuach::PreEscaped<&'static str> =
    cuach::PreEscaped("color:#007bff;text-decoration:none;");
/*
pub const EMAIL_CHARSET: &'static str = "UTF-8";
pub const BLOCKQUOTE_STYLE: cuach::PreEscaped<&'static str> =
    cuach::PreEscaped("border-left:2px solid #666666;padding-left:10px;margin:30px 0 30px 30px;");
*/

impl std::str::FromStr for Error {
    type Err = ();
    fn from_str(input: &str) -> Result<Self, Self::Err> {
        match input {
            "NoSuchUser" => Ok(Error::NoSuchUser),
            "Inactive" => Ok(Error::Inactive),
            "Email" => Ok(Error::Email),
            "LoginAlreadyTaken" => Ok(Error::LoginAlreadyTaken),
            "InvalidLogin" => Ok(Error::InvalidLogin),
            "EmptyLogin" => Ok(Error::EmptyLogin),
            "PasswordMatchError" => Ok(Error::PasswordMatchError),
            "CouldNotSignup" => Ok(Error::CouldNotSignup),
            _ => Err(()),
        }
    }
}

impl cuach::Render for Error {
    type Error = std::fmt::Error;
    fn render_into<W: std::fmt::Write>(&self, w: &mut W) -> Result<(), Self::Error> {
        Ok(write!(w, "{}", self)?)
    }
}

#[template(path = "email/signup.html")]
struct EmailSignupTemplate<'a> {
    host: &'a str,
    token: &'a str,
}

#[template(path = "email/password_reset.html")]
struct EmailPasswordResetTemplate<'a> {
    host: &'a str,
    // login: &'a str,
    token: &'a str,
}

async fn make_email(
    config: &crate::Config,
    db: &mut AsyncPgConnection,
    id: uuid::Uuid,
    is_signup: bool,
    address: String,
) -> Result<rusoto_ses::SendEmailResponse, crate::Error> {
    let mut token = [0u8; 32];
    rng().fill(&mut token[..]);
    use crate::db::tokens::dsl as tokens;
    diesel::insert_into(tokens::tokens)
        .values((tokens::user_id.eq(id), tokens::token.eq(&token)))
        .execute(db)
        .await?;

    debug!(
        "token = {:?} {:?}",
        token,
        data_encoding::HEXLOWER.encode(&token)
    );
    let token = data_encoding::BASE64URL.encode(&token);
    let subject = (if is_signup {
        "Account confirmation"
    } else {
        "Password reset"
    })
    .to_string();

    let mut body_html = String::new();
    let charset = "UTF-8".to_string();
    let body = if is_signup {
        (EmailSignupTemplate {
            host: &config.host,
            token: &token,
        })
        .render_into(&mut body_html)
        .unwrap();
        rusoto_ses::Body {
            text: Some(rusoto_ses::Content {
                data: format!(
                    "Welcome to the nest.

You are just one click away from getting an account, click on the following link:

https://{}/register?token={}
",
                    config.host, token
                ),
                charset: Some(charset.clone()),
            }),
            html: Some(rusoto_ses::Content {
                data: body_html,
                charset: Some(charset),
            }),
        }
    } else {
        (EmailPasswordResetTemplate {
            host: &config.host,
            token: &token,
            // login,
        })
        .render_into(&mut body_html)
        .unwrap();
        rusoto_ses::Body {
            text: Some(rusoto_ses::Content {
                data: format!("Hi,

Someone (hopefully you) asked to reset your password on the Pijul Nest. If this wasn't you, you can just ignore this email.

Else, click on the following link to reset your password:

https://{}/password_reset?token={}
", config.host, token),
                charset: Some(charset.clone())
            }),
            html: Some(rusoto_ses::Content {
                data: body_html,
                charset: Some(charset)
            })
        }
    };
    Ok(crate::email::send_email(config, subject, body, address).await?)
}