use crate::{db, AppState};
use actix_web::{delete, get, post, put, web, HttpRequest, HttpResponse, Responder};
use std::collections::HashMap;
use std::ops::Deref;
use rand::distributions::Alphanumeric;
use rand::{Rng, thread_rng};
use sqlx::QueryBuilder;

pub(crate) mod schemas;

fn get_auth_header(req: &HttpRequest) -> Option<&str> {
    req.headers().get("x-api-key")?.to_str().ok()
}

async fn verify_user_auth(data: &web::Data<AppState>, auth_token: &str, user_token: &str, table: schemas::TablePermission, permissions_only: bool) -> bool {
    let user: db::schemas::User = match sqlx::query_as!(
        db::schemas::User,
        "SELECT * FROM public.users WHERE token = $1",
        auth_token
    )
        .fetch_one(&data.db)
        .await
    {
        Ok(data) => data,
        Err(_) => return false,
    };

    if user.token == user_token && !permissions_only {
        return true;
    }
    else {
        match table {
            schemas::TablePermission::Game => {
                return user.data_permissions;
            },
            schemas::TablePermission::Empire => {
                return user.empire_permissions;
            },
            schemas::TablePermission::Data => {
                return user.data_permissions;
            },
            schemas::TablePermission::User => {
                return user.user_permissions;
            },
        }
    }
}

// User Endpoints
#[utoipa::path(
    request_body = GetUserParams,
    responses(
        (status = 200, description = "OK", body = User),
        (status = 403, description = "Unauthorized"),
        (status = 500, description = "Internal Server Error")
    ),
    security(
        ("api_key" = [])
    ),
)]
#[post("/api/v1/user")]
pub(crate) async fn get_user(
    data: web::Data<AppState>,
    params: web::Json<schemas::GetUserParams>,
    req: HttpRequest,
) -> impl Responder {
    let auth_header = get_auth_header(&req);
    let params = params.into_inner();

    let auth_token: String;

    match auth_header {
        Some(token) => auth_token = token.to_string(),
        None => return HttpResponse::Unauthorized().finish(),
    };

    let auth = verify_user_auth(&data, &auth_token, &params.user_token, schemas::TablePermission::User, false).await;

    if auth {
        let user: db::schemas::User = match sqlx::query_as!(
        db::schemas::User,
        "SELECT * FROM public.users WHERE token = $1",
        params.user_token
    )
            .fetch_one(&data.db)
            .await
        {
            Ok(data) => data,
            Err(_) => return HttpResponse::InternalServerError().finish(),
        };

        let mut user_permissions: HashMap<String, bool> = HashMap::new();

        user_permissions.insert("game_permissions".to_string(), user.game_permissions);
        user_permissions.insert("empire_permissions".to_string(), user.empire_permissions);
        user_permissions.insert("data_permissions".to_string(), user.data_permissions);
        user_permissions.insert("user_permissions".to_string(), user.user_permissions);

        let return_data = schemas::User {
            user_token: user.token,
            discord_handle: user.discord_id,
            profile_picture: user.picture_url,
            permissions: user_permissions
        };

        return HttpResponse::Ok().json(return_data);
    }
    else {
        return HttpResponse::Unauthorized().finish();
    }
}

#[utoipa::path(
    responses(
        (status = 200, description = "OK", body = User),
        (status = 500, description = "Internal Server Error")
    ),
)]
#[post("/api/v1/user/create")]
pub(crate) async fn create_user(
    data: web::Data<AppState>,
) -> impl Responder {
    let user: db::schemas::User;

    let mut rng = thread_rng();

    let user_tokens = match sqlx::query_scalar!(
        "SELECT token FROM public.users"
    )
        .fetch_all(&data.db)
        .await
    {
        Ok(data) => data,
        Err(_) => return HttpResponse::InternalServerError().finish(),
    };

    let new_token: String;

    loop {
        let mut chars: String = (0..6).map(|_| rng.sample(Alphanumeric) as char).collect();
        chars = chars.to_uppercase();
        if !user_tokens.contains(&chars) {
            new_token = chars;
            break;
        }
        else {
            println!("looping");
        }
    }

    user = match sqlx::query_as!(
        db::schemas::User,
        "INSERT INTO public.users(token, game_permissions, empire_permissions, data_permissions, user_permissions) VALUES ($1, $2, $3, $4, $5) RETURNING * ",
        new_token,
        false,
        false,
        false,
        false

    )
        .fetch_one(&data.db)
        .await
    {
        Ok(data) => data,
        Err(_) => return HttpResponse::InternalServerError().finish(),
    };

    return HttpResponse::Ok().json(user)
}

#[utoipa::path(
    request_body = UpdateUserParams,
    responses(
        (status = 200, description = "OK"),
        (status = 403, description = "Unauthorized"),
        (status = 500, description = "Internal Server Error")
    ),
    security(
        ("api_key" = [])
    ),
)]
#[put("/api/v1/user")]
pub(crate) async fn update_user(
    data: web::Data<AppState>,
    params: web::Json<schemas::UpdateUserParams>,
    req: HttpRequest,
) -> impl Responder {
    let auth_header = get_auth_header(&req);
    let params = params.into_inner();

    let auth_token: String;

    match auth_header {
        Some(token) => auth_token = token.to_string(),
        None => return HttpResponse::Unauthorized().finish(),
    };

    let mut user_permissions: HashMap<String, bool> = HashMap::new();
    match params.permissions {
        Some(data) => {user_permissions = data.clone()},
        None => {},
    }

    let mut elevated_auth = false;
    if user_permissions.len() != 0 {
        if user_permissions["game_permissions"] || user_permissions["empire_permissions"] || user_permissions["data_permissions"] || user_permissions["user_permissions"] {
            elevated_auth = true;
        }
    }

    let auth = verify_user_auth(&data, &auth_token, &params.user_token, schemas::TablePermission::User, elevated_auth).await;

    // SQL Queries
    // TODO: Optimize by utilizing some SQL magic, for now this has to do
    if auth {
        let user: db::schemas::User;

        let mut user_query = QueryBuilder::<sqlx::Postgres>::new("UPDATE public.users SET ");
        let mut user_query_separated = user_query.separated(", ");
        let mut any_param_present = false;

        if let Some(discord_handle) = params.discord_handle {
            user_query_separated.push(" discord_id = ").push_bind_unseparated(discord_handle);
            any_param_present = true;
        }

        if let Some(profile_picture) = params.profile_picture {
            user_query_separated.push(" picture_url = ");
            match any_param_present {
                true => user_query_separated.push_bind(profile_picture),
                false => user_query_separated.push_bind_unseparated(profile_picture)
            };
            any_param_present = true;
        }

        if user_permissions.len() != 0 {
            for (entry, value) in user_permissions.iter() {
                match entry.deref() {
                    "game_permissions" => {
                        user_query_separated.push( " game_permissions = ");
                        match any_param_present {
                            true => user_query_separated.push_bind(value),
                            false => user_query_separated.push_bind_unseparated(value)
                        };
                        any_param_present = true;
                    },
                    "empire_permissions" => {
                        user_query_separated.push( " empire_permissions = ");
                        match any_param_present {
                            true => user_query_separated.push_bind(value),
                            false => user_query_separated.push_bind_unseparated(value)
                        };
                        any_param_present = true;
                    },
                    "data_permissions" => {
                        user_query_separated.push( " data_permissions = ");
                        match any_param_present {
                            true => user_query_separated.push_bind(value),
                            false => user_query_separated.push_bind_unseparated(value)
                        };
                        any_param_present = true;
                    },
                    "user_permissions" => {
                        user_query_separated.push( " user_permissions = ");
                        match any_param_present {
                            true => user_query_separated.push_bind(value),
                            false => user_query_separated.push_bind_unseparated(value)
                        };
                        any_param_present = true;
                    },
                    _ => {}
                }
            }
        }

        if any_param_present {
            user_query_separated.push_unseparated(" WHERE token = ").push_bind_unseparated(params.user_token);
            user_query_separated.push_unseparated(" RETURNING *");

            user = match user_query
                .build_query_as::<db::schemas::User>()
                .fetch_one(&data.db)
                .await
            {
                Ok(data) => data,
                Err(_) => return HttpResponse::InternalServerError().finish(),
            };
        } else {
            user = match sqlx::query_as!(
                db::schemas::User,
                "SELECT * FROM public.users WHERE token = $1",
                params.user_token
            )
                .fetch_one(&data.db)
                .await
            {
                Ok(data) => data,
                Err(_) => return HttpResponse::InternalServerError().finish(),
            };
        }

        let mut user_permissions: HashMap<String, bool> = HashMap::new();
        user_permissions.insert("game_permissions".to_string(), user.game_permissions);
        user_permissions.insert("empire_permissions".to_string(), user.empire_permissions);
        user_permissions.insert("data_permissions".to_string(), user.data_permissions);
        user_permissions.insert("user_permissions".to_string(), user.user_permissions);

        let return_data = schemas::User {
            user_token: user.token,
            discord_handle: user.discord_id,
            profile_picture: user.picture_url,
            permissions: user_permissions
        };
        return HttpResponse::Ok().json(return_data);
    } else {
        return HttpResponse::Unauthorized().finish();
    }
}

#[utoipa::path(
    request_body = DeleteUserParams,
    responses(
        (status = 200, description = "OK"),
        (status = 403, description = "Unauthorized"),
        (status = 500, description = "Internal Server Error")
    ),
    security(
        ("api_key" = [])
    ),
)]
#[delete("/api/v1/user")]
pub(crate) async fn delete_user(
    data: web::Data<AppState>,
    params: web::Query<schemas::DeleteUserParams>,
    req: HttpRequest,
) -> impl Responder {
    let auth_header = get_auth_header(&req);
    let params = params.into_inner();

    let auth_token: String;

    match auth_header {
        Some(token) => auth_token = token.to_string(),
        None => return HttpResponse::Unauthorized().finish(),
    };

    let auth = verify_user_auth(&data, &auth_token, &params.user_token, schemas::TablePermission::User, false).await;

    // SQL Queries
    // TODO: Optimize by utilizing some SQL magic, for now this has to do
    if auth {
        match sqlx::query!(
            "DELETE FROM public.users WHERE token = $1",
            params.user_token
        )
            .execute(&data.db)
            .await
        {
            Ok(_) => {}
            Err(_) => {
                return HttpResponse::InternalServerError().finish();
            }
        };

        return HttpResponse::Ok().into();
    } else {
        return HttpResponse::Unauthorized().finish();
    }
}