use anyhow::{Context, Result};
use async_session::{Session, SessionStore as _};
use axum::{
    extract::{FromRequestParts, Query, State},
    http::request::Parts,
    response::{IntoResponse, Redirect, Response},
    routing::get,
    RequestPartsExt, Router,
};
use axum_extra::{
    extract::cookie::{Cookie, CookieJar, SameSite},
    headers, TypedHeader,
};
use diesel::prelude::*;
use oauth2::{
    basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId,
    ClientSecret, CsrfToken, RedirectUrl, TokenResponse, TokenUrl,
};
use serde::{Deserialize, Serialize};
use tracing::{debug, trace_span};

use crate::{app_error::AppError, app_state::AppState, schema, settings::Settings};

pub fn new_oauth_client(settings: &Settings) -> Result<BasicClient, AppError> {
    Ok(BasicClient::new(
        ClientId::new(settings.auth.client_id.clone()),
        Some(ClientSecret::new(settings.auth.client_secret.clone())),
        AuthUrl::new(settings.auth.auth_url.clone())
            .context("failed to create new authorization server URL")?,
        Some(
            TokenUrl::new(settings.auth.token_url.clone())
                .context("failed to create new token endpoint URL")?,
        ),
    )
    .set_redirect_uri(
        RedirectUrl::new(settings.auth.redirect_url.clone())
            .context("failed to create new redirection URL")?,
    ))
}

pub fn new_router() -> Router<AppState> {
    Router::new()
        .route("/login", get(propel_auth))
        .route("/callback", get(login_authorized))
        .route("/logout", get(logout))
}

pub async fn propel_auth(State(state): State<AppState>) -> impl IntoResponse {
    let csrf_token = CsrfToken::new_random();
    let (auth_url, _csrf_token) = state
        .oauth_client
        .authorize_url(|| csrf_token)
        // .add_scopes(vec![Scope::new("openid".to_string())])
        .url();
    // FIXME: check CSRF token
    Redirect::to(auth_url.as_ref())
}

pub async fn logout(
    State(state): State<AppState>,
    TypedHeader(cookies): TypedHeader<headers::Cookie>,
) -> Result<impl IntoResponse, AppError> {
    let cookie = cookies
        .get(state.settings.auth.cookie_name.as_str())
        .context("couldn't get session cookie")?;
    let session_id = Session::id_from_cookie_value(cookie)?;
    state
        .db_pool
        .get()
        .await?
        .interact(move |conn| {
            diesel::delete(schema::browser_sessions::table)
                .filter(schema::browser_sessions::id.eq(session_id))
                .execute(conn)
        })
        .await
        .unwrap()?;
    // FIXME: call logout endpoint of OIDC provider
    Ok(Redirect::to(&state.settings.base_path))
}

#[derive(Debug, Deserialize)]
pub struct AuthRequestQuery {
    code: String,
    state: String, // CSRF token
}

pub const AUTH_INFO_SESSION_KEY: &'static str = "user";

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

async fn get_user_info(
    settings: &Settings,
    access_token: &oauth2::AccessToken,
) -> Result<AuthInfo, AppError> {
    let client = reqwest::Client::new();
    Ok(client
        .get(settings.auth.userinfo_url.as_str())
        .bearer_auth(access_token.secret())
        .send()
        .await?
        .json()
        .await?)
}

pub async fn login_authorized(
    Query(query): Query<AuthRequestQuery>,
    State(state): State<AppState>,
    jar: CookieJar,
) -> Result<impl IntoResponse, AppError> {
    let response = state
        .oauth_client
        .exchange_code(AuthorizationCode::new(query.code.clone()))
        .request_async(async_http_client)
        .await?;
    let user_info = get_user_info(&state.settings, response.access_token()).await?;
    let mut session = Session::new();
    session.insert(AUTH_INFO_SESSION_KEY, &user_info)?;
    let cookie_value = state
        .session_store
        .store_session(session)
        .await?
        .context("cookie value from store_session() is None")?;
    let jar = jar.add(
        Cookie::build((state.settings.auth.cookie_name.clone(), cookie_value))
            .same_site(SameSite::Lax)
            .http_only(true)
            .path("/"),
    );
    Ok((jar, Redirect::to(&format!("{}/", state.settings.base_path))))
}

pub struct AuthRedirect {
    base_path: String,
}

impl AuthRedirect {
    pub fn new(base_path: &str) -> Self {
        Self {
            base_path: base_path.to_string(),
        }
    }
}

impl IntoResponse for AuthRedirect {
    fn into_response(self) -> Response {
        Redirect::to(&format!("{}/auth/login", self.base_path)).into_response()
    }
}

impl FromRequestParts<AppState> for AuthInfo {
    type Rejection = AuthRedirect;

    async fn from_request_parts(
        parts: &mut Parts,
        state: &AppState,
    ) -> Result<Self, <Self as FromRequestParts<AppState>>::Rejection> {
        let _ = trace_span!("AuthInfo from_request_parts()").enter();
        let jar = parts.extract::<CookieJar>().await.unwrap();
        let session_cookie = jar
            .get(&state.settings.auth.cookie_name)
            .ok_or(AuthRedirect::new(&state.settings.base_path))?;
        debug!("session cookie loaded");
        let session = state
            .session_store
            .load_session(session_cookie.value().to_string())
            .await
            .unwrap()
            .ok_or(AuthRedirect::new(&state.settings.base_path))?;
        debug!("session loaded");
        let user = session
            .get::<AuthInfo>(AUTH_INFO_SESSION_KEY)
            .ok_or(AuthRedirect::new(&state.settings.base_path))?;
        Ok(user)
    }
}
