use axum::debug_handler;
use axum::http::header;
use axum::http::HeaderValue;
use axum::response::Redirect;
use cookie::Cookie;
use cookie::CookieJar;
use loco_rs::prelude::*;
use serde::{Deserialize, Serialize};

use crate::{
    mailers::auth::AuthMailer,
    models::{_entities::users, users::LoginParams},
    views::auth::CurrentResponse,
};
#[derive(Debug, Deserialize, Serialize)]
pub struct VerifyParams {
    pub token: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ForgotParams {
    pub email: String,
}

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

/// Verify register user. if the user not verified his email, he can't login to
/// the system.
#[debug_handler]
async fn verify(
    State(ctx): State<AppContext>,
    Json(params): Json<VerifyParams>,
) -> Result<Response> {
    let user = users::Model::find_by_verification_token(&ctx.db, &params.token).await?;

    if user.email_verified_at.is_some() {
        tracing::info!(pid = user.pid.to_string(), "user already verified");
    } else {
        let active_model = user.into_active_model();
        let user = active_model.verified(&ctx.db).await?;
        tracing::info!(pid = user.pid.to_string(), "user verified");
    }

    format::json(())
}

/// In case the user forgot his password  this endpoints generate a forgot token
/// and send email to the user. In case the email not found in our DB, we are
/// returning a valid request for for security reasons (not exposing users DB
/// list).
#[debug_handler]
async fn forgot(
    State(ctx): State<AppContext>,
    Json(params): Json<ForgotParams>,
) -> Result<Response> {
    let Ok(user) = users::Model::find_by_email(&ctx.db, &params.email).await else {
        // we don't want to expose our users email. if the email is invalid we still
        // returning success to the caller
        return format::json(());
    };

    let user = user
        .into_active_model()
        .set_forgot_password_sent(&ctx.db)
        .await?;

    AuthMailer::forgot_password(&ctx, &user).await?;

    format::json(())
}

/// reset user password by the given parameters
#[debug_handler]
async fn reset(State(ctx): State<AppContext>, Json(params): Json<ResetParams>) -> Result<Response> {
    let Ok(user) = users::Model::find_by_reset_token(&ctx.db, &params.token).await else {
        // we don't want to expose our users email. if the email is invalid we still
        // returning success to the caller
        tracing::info!("reset token not found");

        return format::json(());
    };
    user.into_active_model()
        .reset_password(&ctx.db, &params.password)
        .await?;

    format::json(())
}

/// Creates a user login and returns a token
#[debug_handler]
async fn login(
    State(ctx): State<AppContext>,
    jar: CookieJar,
    Form(params): Form<LoginParams>,
) -> Result<Response> {
    let user = users::Model::find_by_email(&ctx.db, &params.email).await?;

    let valid = user.verify_password(&params.password);

    if !valid {
        return unauthorized("unauthorized!");
    }

    let jwt_secret = ctx.config.get_jwt_config()?;

    let token = user
        .generate_jwt(&jwt_secret.secret, &jwt_secret.expiration)
        .or_else(|_| unauthorized("unauthorized!"))?;

    let cookie = Cookie::build(("token", token.clone()))
        .path("/")
        .http_only(true)
        .secure(true)
        .same_site(cookie::SameSite::Lax);
    let cookie = jar.add(cookie);

    let cookie = cookie.get("token").map(|c| c.value().to_owned());

    let mut response = Redirect::to("/").into_response();
    match cookie {
        Some(token) => {
            let cookie = format!("token={}; Path=/; HttpOnly; Secure; SameSite=Lax", token);
            let authorization_header = format!("Bearer {}", token);
            response
                .headers_mut()
                .append(header::SET_COOKIE, HeaderValue::from_str(&cookie).unwrap());
            response.headers_mut().append(
                header::AUTHORIZATION,
                HeaderValue::from_str(&authorization_header).unwrap(),
            );
        }
        None => {}
    }

    Ok(response)
}

#[debug_handler]
async fn current(auth: auth::JWT, State(ctx): State<AppContext>) -> Result<Response> {
    let user = users::Model::find_by_pid(&ctx.db, &auth.claims.pid).await?;
    format::json(CurrentResponse::new(&user))
}

async fn logout() -> impl IntoResponse {
    let cookie = "token=''; Path=/; HttpOnly; Secure; SameSite=Lax";
    let mut response = Redirect::to("/").into_response();

    response
        .headers_mut()
        .append(header::SET_COOKIE, HeaderValue::from_str(&cookie).unwrap());

    response
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("/api/auth")
        .add("/verify", post(verify))
        .add("/login", post(login))
        .add("/forgot", post(forgot))
        .add("/reset", post(reset))
        .add("/current", get(current))
        .add("/logout", get(logout))
}
