use std::{collections::HashMap, vec};

use actix_web::{
    delete, get, post, put,
    web::{self, Json},
    HttpResponse, Responder,
};

use crate::{db, AppState};

pub(crate) mod schemas;

fn verify_auth(token: &str, data: &AppState) -> schemas::AuthReturn {
    let mut auth_return = schemas::AuthReturn {
        moderator: false,
        admin: false,
    };

    if token == data.auth_tokens.admin {
        auth_return.admin = true;
        auth_return.moderator = true;
    } else if token == data.auth_tokens.moderator {
        auth_return.moderator = true;
    }

    return auth_return;
}

#[utoipa::path(
    params(
        schemas::AuthParams
    ),
    responses(
        (status = 200, description = "OK", body = AuthReturn),
    ),
    security(
        ("api_key" = [])
    ),
)]
#[get("/api/v3/auth")]
pub(crate) async fn auth(
    data: web::Data<AppState>,
    params: web::Query<schemas::AuthParamsOptional>,
) -> impl Responder {
    let params: schemas::AuthParamsOptional = params.into_inner();

    let mut auth_return = schemas::AuthReturn {
        moderator: false,
        admin: false,
    };

    if let Some(auth_token) = params.token.clone() {
        if auth_token == data.auth_tokens.admin {
            auth_return.admin = true;
            auth_return.moderator = true;
        } else if auth_token == data.auth_tokens.moderator {
            auth_return.moderator = true;
        }
    }

    Json(auth_return)
}

#[utoipa::path(
    params(

    ),
    responses(
        (status = 200, description = "OK", body = FullViewData),
    ),
    security(
        ("api_key" = [])
    ),
)]
#[get("/api/v3/full_view_data")]
pub(crate) async fn full_view_data(data: web::Data<AppState>) -> impl Responder {
    let start = chrono::Local::now(); // DEBUG

    // SQL Queries
    // TODO: Optimize by utilizing some SQL magic, for now this has to do

    let db_games: Vec<db::schemas::Game> = sqlx::query_as("SELECT * FROM public.games ORDER BY id")
        .fetch_all(&data.db)
        .await
        .expect("Error Fetching Data from DB");

    let db_groups: Vec<db::schemas::GameGroup> =
        sqlx::query_as("SELECT * FROM public.game_groups ORDER BY id")
            .fetch_all(&data.db)
            .await
            .expect("Error Fetching Data from DB");

    let db_empires: Vec<db::schemas::Empire> =
        sqlx::query_as("SELECT * FROM public.empires ORDER BY id")
            .fetch_all(&data.db)
            .await
            .expect("Error Fetching Data from DB");

    let db_ethics: Vec<db::schemas::Ethic> =
        sqlx::query_as("SELECT * FROM public.ethics ORDER BY id")
            .fetch_all(&data.db)
            .await
            .expect("Error Fetching Data from DB");

    let db_empire_ethics: Vec<db::schemas::EmpireEthic> =
        sqlx::query_as("SELECT * FROM public.empire_ethics ORDER BY empires_id")
            .fetch_all(&data.db)
            .await
            .expect("Error Fetching Data from DB");

    let db_portrait_groups: Vec<db::schemas::PortraitGroup> =
        sqlx::query_as("SELECT * FROM public.portrait_groups ORDER BY id")
            .fetch_all(&data.db)
            .await
            .expect("Error Fetching Data from DB");

    let db_portraits: Vec<db::schemas::Portrait> =
        sqlx::query_as("SELECT * FROM public.portraits ORDER BY id")
            .fetch_all(&data.db)
            .await
            .expect("Error Fetching Data from DB");

    let mut parsed_data: schemas::FullViewData = schemas::FullViewData {
        games: HashMap::new(),
        ethics: HashMap::new(),
        species: HashMap::new(),
    };

    // Data processing
    // Species Vector
    db_portrait_groups.iter().for_each(|species| {
        let new_data = schemas::Species {
            id: species.id,
            displayName: species.name.clone(),
            portraits: HashMap::new(),
        };

        parsed_data
            .species
            .entry(species.id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    db_portraits.iter().for_each(|portrait| {
        let new_data = schemas::Portrait {
            id: portrait.id,
            hires: portrait.hires.clone(),
            lores: portrait.lores.clone(),
        };

        parsed_data
            .species
            .get_mut(&portrait.group_id)
            .unwrap()
            .portraits
            .entry(portrait.id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    // Games Vector and Children
    db_games.iter().for_each(|game| {
        let new_data = schemas::ChellarisGame {
            id: game.id,
            name: game.name.clone(),
            empires: HashMap::new(),
            groups: HashMap::new(),
        };

        parsed_data
            .games
            .entry(game.id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    db_groups.iter().for_each(|group| {
        let new_data = schemas::ChellarisGameGroup {
            id: group.id,
            name: group.name.clone(),
        };

        parsed_data
            .games
            .get_mut(&group.game_id)
            .unwrap()
            .groups
            .entry(group.id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    db_empires.iter().for_each(|empire| {
        let new_data = schemas::ChellarisEmpire {
            id: empire.id,
            gestalt: empire.gestalt.unwrap_or(false),
            machine: false,
            group: empire.group_id,
            empire_portrait: empire.empire_portrait_id,
            empire_portrait_group: empire.empire_portrait_group_id,
            discord_user: empire.discord_user.clone(),
            ethics: HashMap::new(),
        };

        parsed_data
            .games
            .get_mut(&empire.group_game_id)
            .unwrap()
            .empires
            .entry(empire.id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    // Ethics Vector
    db_ethics.iter().for_each(|ethic| {
        let new_data = schemas::Ethic {
            id: ethic.id,
            displayName: ethic.name.clone(),
            machine: ethic.machine_ethic,
        };

        parsed_data
            .ethics
            .entry(ethic.id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    db_empire_ethics.iter().for_each(|empire_ethic| {
        let game_id = empire_ethic.empires_group_game_id;
        let id = empire_ethic.empires_id;

        let new_data = schemas::EmpireEthic {
            id: empire_ethic.ethics_id,
            displayName: parsed_data.ethics[&empire_ethic.ethics_id]
                .displayName
                .clone(),
            machine: parsed_data.ethics[&empire_ethic.ethics_id].machine,
            fanatic: empire_ethic.ethics_fanatic,
        };

        if new_data.machine {
            parsed_data
                .games
                .get_mut(&game_id)
                .unwrap()
                .empires
                .get_mut(&id)
                .unwrap()
                .machine = true;
        }

        parsed_data
            .games
            .get_mut(&game_id)
            .unwrap()
            .empires
            .get_mut(&id)
            .unwrap()
            .ethics
            .entry(empire_ethic.ethics_id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    println!("{:?} ms", (chrono::Local::now() - start).to_std().unwrap()); // DEBUG

    Json(parsed_data)
}

// Data Fetching Endpoints for Admin/Moderator Menu

#[utoipa::path(
    params(),
    responses(
        (status = 200, description = "OK", body = HashMap<i32, ChellarisGameLite>),
    ),
    security(
        ("api_key" = [])
    ),
)]
#[get("/api/v3/list_games")]
pub(crate) async fn list_games(data: web::Data<AppState>) -> impl Responder {
    // SQL Queries
    // TODO: Optimize by utilizing some SQL magic, for now this has to do

    let db_games: Vec<db::schemas::Game> = sqlx::query_as("SELECT * FROM public.games ORDER BY id")
        .fetch_all(&data.db)
        .await
        .expect("Error Fetching Data from DB");

    let mut parsed_data: HashMap<i32, schemas::ChellarisGameLite> = HashMap::new();

    // Data processing
    // Games Vector
    db_games.iter().for_each(|game| {
        let new_data = schemas::ChellarisGameLite {
            id: game.id,
            name: game.name.clone(),
        };

        parsed_data
            .entry(game.id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    Json(parsed_data)
}

#[utoipa::path(
    params(
        schemas::GetGameParam
    ),
    responses(
        (status = 200, description = "OK", body = HashMap<i32, ChellarisGameLite>),
    ),
    security(
        ("api_key" = [])
    ),
)]
#[get("/api/v3/game")]
pub(crate) async fn get_game_data(
    data: web::Data<AppState>,
    params: web::Query<schemas::GetGameParam>,
) -> impl Responder {
    let params: schemas::GetGameParam = params.into_inner();

    // SQL Queries
    // TODO: Optimize by utilizing some SQL magic, for now this has to do
    let db_games: Vec<db::schemas::Game>;
    if let Some(game_id) = params.game_id {
        db_games = match sqlx::query_as!(
            db::schemas::Game,
            "SELECT * FROM public.games WHERE id = $1 ORDER BY id",
            game_id as i32
        )
        .fetch_one(&data.db)
        .await
        {
            Ok(data) => vec![data],
            Err(_) => vec![],
        };
    } else {
        db_games = sqlx::query_as("SELECT * FROM public.games ORDER BY id")
            .fetch_all(&data.db)
            .await
            .expect("Error Fetching Data from DB");
    }

    let mut parsed_data: HashMap<i32, schemas::ChellarisGameLite> = HashMap::new();

    // Data processing
    // Games Vector
    db_games.iter().for_each(|game| {
        let new_data = schemas::ChellarisGameLite {
            id: game.id,
            name: game.name.clone(),
        };

        parsed_data
            .entry(game.id)
            .and_modify(|d| *d = new_data.clone())
            .or_insert(new_data);
    });

    Json(parsed_data)
}

#[utoipa::path(
    request_body = PostGameParams,
    responses(
        (status = 200, description = "OK", body = ChellarisGameLite),
        (status = 422, description = "Missing Game Name"),
        (status = 401, description = "Auth Token Invalid"),
    ),
    security(
        ("api_key" = [])
    ),
)]
#[post("/api/v3/game")]
pub(crate) async fn create_game(
    data: web::Data<AppState>,
    params: web::Json<schemas::PostGameParams>,
) -> impl Responder {
    let params = params.into_inner();

    let user_auth: schemas::AuthReturn = verify_auth(&params.auth.token, &data);

    // SQL Queries
    // TODO: Optimize by utilizing some SQL magic, for now this has to do
    if user_auth.admin || user_auth.moderator {
        if params.game_name != "" {
            let db_game: db::schemas::Game;

            db_game = match sqlx::query_as!(
                db::schemas::Game,
                "INSERT INTO public.games(name) VALUES ($1) RETURNING * ",
                params.game_name
            )
            .fetch_one(&data.db)
            .await
            {
                Ok(data) => data,
                Err(_) => return HttpResponse::UnprocessableEntity().finish(),
            };

            let parsed_game: schemas::ChellarisGameLite = schemas::ChellarisGameLite {
                id: db_game.id,
                name: db_game.name,
            };

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

#[utoipa::path(
    params(
        schemas::DeleteGameParam,
    ),
    request_body = AuthParams,
    responses(
        (status = 200, description = "OK"),
        (status = 422, description = "Missing Game ID"),
        (status = 401, description = "Auth Token Invalid"),
    ),
    security(
        ("api_key" = [])
    ),
)]
#[delete("/api/v3/game")]
pub(crate) async fn delete_game(
    data: web::Data<AppState>,
    auth_params: web::Json<schemas::AuthParams>,
    param: web::Query<schemas::DeleteGameParam>,
) -> impl Responder {
    let auth_params = auth_params.into_inner();
    let param = param.into_inner();

    let user_auth: schemas::AuthReturn = verify_auth(&auth_params.token, &data);

    // SQL Queries
    // TODO: Optimize by utilizing some SQL magic, for now this has to do
    if user_auth.admin || user_auth.moderator {
        match sqlx::query!(
            "DELETE FROM public.games WHERE id = $1",
            param.game_id as i32
        )
        .execute(&data.db)
        .await
        {
            Ok(_) => {},
            Err(_) => return HttpResponse::UnprocessableEntity().finish(),
        };

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

// Data Manipulation Endpoints

// Moderator & Admin
// Add/Update/Remove Empire
// Add/Update/Remove Group
// Add/Update/Remove Game

// Admin
// Add/Update/Remove Portrait
// Add/Update/Remove Species
// Add/Update/Remove Ethics