use async_session::{MemoryStore, Session, SessionStore as _};
use axum::{
    extract::{Extension, Query},
    http::{header::HeaderMap, header::SET_COOKIE},
    response::{IntoResponse, Redirect},
};
use oauth2::{
    basic::BasicClient, reqwest::async_http_client, AuthUrl, AuthorizationCode, ClientId,
    ClientSecret, CsrfToken, PkceCodeChallenge, RedirectUrl, Scope, TokenResponse, TokenUrl,
};
use serde::Deserialize;
use std::{collections::HashMap, env};

use crate::User;
use crate::COOKIE_NAME;

#[derive(Debug, Deserialize)]
#[allow(dead_code)]
pub struct AuthRequest {
    code: String,
    state: String,
}

pub async fn discord_auth() -> impl IntoResponse {
    let discord_oauth_client = discord_oauth_client();
    let (auth_url, _csrf_token) = discord_oauth_client
        .authorize_url(CsrfToken::new_random)
        .add_scope(Scope::new("identify".to_string()))
        .url();

    // Redirect to Discord's oauth service
    Redirect::to(&auth_url.to_string())
}

pub async fn discord_authorized(
    Query(query): Query<AuthRequest>,
    Extension(store): Extension<MemoryStore>,
    Extension(oauth_clients): Extension<HashMap<&str, BasicClient>>,
) -> impl IntoResponse {
    // Check for Discord client
    if oauth_clients.contains_key("Discord") {
        // Get Discord client
        let discord_oauth_client = oauth_clients.get(&"Discord").unwrap();

        // Get an auth token
        let token = discord_oauth_client
            .exchange_code(AuthorizationCode::new(query.code.clone()))
            .request_async(async_http_client)
            .await
            .unwrap();

        // Fetch user data from discord
        let client = reqwest::Client::new();
        let user_data: User = client
            // https://discord.com/developers/docs/resources/user#get-current-user
            .get("https://discordapp.com/api/users/@me")
            .bearer_auth(token.access_token().secret())
            .send()
            .await
            .unwrap()
            .json::<User>()
            .await
            .unwrap();

        // Create a new session filled with user data
        let mut session = Session::new();
        session.insert("user", &user_data).unwrap();

        // Store session and get corresponding cookie
        let cookie = store.store_session(session).await.unwrap().unwrap();

        // Build the cookie
        let cookie = format!("{}={}; SameSite=Lax; Path=/", COOKIE_NAME, cookie);

        // Set cookie
        let mut headers = HeaderMap::new();
        headers.insert(SET_COOKIE, cookie.parse().unwrap());

        (headers, Redirect::to(&"/dashboard"))
    } else {
        let headers = HeaderMap::new();

        (headers, Redirect::to(&"/"))
    }
}

pub fn discord_oauth_client() -> BasicClient {
    let redirect_url = env::var("REDIRECT_URL")
        .unwrap_or_else(|_| "http://localhost:40192/auth/discord".to_string());
    // .unwrap_or_else(|_| "https://www.jean-marie.ca/auth/discord".to_string());

    let discord_client_id = env::var("DISCORD_CLIENT_ID").expect("Missing DISCORD_CLIENT_ID!");
    let discord_client_secret =
        env::var("DISCORD_CLIENT_SECRET").expect("Missing DISCORD_CLIENT_SECRET!");
    let discord_auth_url = env::var("DISCORD_AUTH_URL").unwrap_or_else(|_| {
        "https://discord.com/api/oauth2/authorize?response_type=code".to_string()
    });
    let discord_token_url = env::var("DISCORD_TOKEN_URL")
        .unwrap_or_else(|_| "https://discord.com/api/oauth2/token".to_string());

    BasicClient::new(
        ClientId::new(discord_client_id),
        Some(ClientSecret::new(discord_client_secret)),
        AuthUrl::new(discord_auth_url).unwrap(),
        Some(TokenUrl::new(discord_token_url).unwrap()),
    )
    .set_redirect_uri(RedirectUrl::new(redirect_url).unwrap())
}
