use axum::{
    body::Empty,
    http::{Request, Response, StatusCode},
    middleware,
    response::IntoResponse,
    Extension, Form,
};
use pbkdf2::{
    password_hash::{PasswordHash, PasswordVerifier},
    Pbkdf2,
};
use sqlx::PgPool;

use crate::{
    database::{
        session::{new_session, Random},
        user::create_user,
    },
    errors::{LoginError, SignupError},
    html::{
        root::{error_page, get_login},
        Login, Signup,
    },
};

#[derive(Clone)]
pub struct UserInfo {
    pub user_id: i32,
    pub admin: bool,
}

#[derive(Clone)]
pub struct AuthState(Option<(u128, Option<UserInfo>, PgPool)>);

pub async fn auth<B>(
    mut req: Request<B>,
    next: middleware::Next<B>,
    pool: PgPool,
) -> axum::response::Response {
    let session_token = req
        .headers()
        .get_all("Cookie")
        .iter()
        .filter_map(|cookie| {
            cookie
                .to_str()
                .ok()
                .and_then(|cookie| cookie.parse::<cookie::Cookie>().ok())
        })
        .find_map(|cookie| {
            (cookie.name() == "session_token").then(move || cookie.value().to_owned())
        })
        .and_then(|cookie_value| cookie_value.parse::<u128>().ok());

    if session_token.is_none()
        && req.uri().to_string().contains("/admin")
        && !req.uri().to_string().contains("/login")
    {
        return get_login().await.into_response();
    }

    req.extensions_mut()
        .insert(AuthState(session_token.map(|v| (v, None, pool))));

    next.run(req).await
}

impl AuthState {
    pub async fn get_user(&mut self) -> Option<&UserInfo> {
        let (session_token, store, pool) = self.0.as_mut()?;

        if store.is_none() {
            const QUERY: &str =
                "SELECT id, admin FROM users JOIN sessions ON user_id = id WHERE session_token = $1;";
            let user: Option<(i32, bool)> = sqlx::query_as(QUERY)
                .bind(&session_token.to_le_bytes().to_vec())
                .fetch_optional(&*pool)
                .await
                .unwrap();

            if let Some((id, admin)) = user {
                *store = Some(UserInfo { user_id: id, admin });
            }
        }
        store.as_ref()
    }
}

pub fn set_cookie(session_token: &str) -> impl IntoResponse {
    Response::builder()
        .status(StatusCode::SEE_OTHER)
        .header("Location", "/")
        .header(
            "Set-Cookie",
            format!("session_token={}; Max-Age=999999", session_token),
        )
        .body(Empty::new())
        .unwrap()
}

pub async fn post_login(
    Extension(pool): Extension<PgPool>,
    Extension(random): Extension<Random>,
    Form(login): Form<Login>,
) -> impl IntoResponse {
    const LOGIN_QUERY: &str = "SELECT id, password FROM users WHERE users.username = $1;";

    let row: Option<(i32, String)> = sqlx::query_as(LOGIN_QUERY)
        .bind(login.username)
        .fetch_optional(&pool)
        .await
        .unwrap();

    let (user_id, hashed_password) = if let Some(row) = row {
        row
    } else {
        return Err(error_page(&LoginError::UserDoesNotExist));
    };

    // Verify password against PHC string
    let parsed_hash = PasswordHash::new(&hashed_password).unwrap();

    if let Err(_err) = Pbkdf2.verify_password(login.password.as_bytes(), &parsed_hash) {
        return Err(error_page(&LoginError::WrongPassword));
    }

    let session_token = new_session(&pool, random, user_id).await;

    Ok(set_cookie(&session_token))
}

pub async fn post_signup(
    Extension(pool): Extension<PgPool>,
    Extension(random): Extension<Random>,
    Form(signup): Form<Signup>,
) -> impl IntoResponse {
    if signup.password != signup.confirm_password {
        return Err(error_page(&SignupError::PasswordsDoNotMatch));
    }

    let user_id = create_user(&signup.username, &signup.password, &pool)
        .await
        .unwrap();

    let session_token = new_session(&pool, random, user_id).await;

    Ok(set_cookie(&session_token))
}

pub async fn logout_response() -> impl IntoResponse {
    Response::builder()
        .status(StatusCode::SEE_OTHER)
        .header("Location", "/")
        .header("Set-Cookie", "session_token=_; Max-Age=0")
        .body(Empty::new())
        .unwrap()
}
