use std::collections::hash_map::Entry;
use std::collections::HashMap;
use rocket::fairing::AdHoc;
use rocket::form::Form;
use rocket::futures::TryFutureExt;
use rocket::http::CookieJar;
use rocket::response::Redirect;
use rocket::serde::{Serialize, Deserialize};
use rocket::serde::json::Json;

use rocket_db_pools::{sqlx, Connection};

use crate::{auth, database};
use crate::database_records::{Vote, VotingData};

#[derive(FromForm)]
pub struct VoteForm {
    truth_votes: HashMap<u32, u16>
}

#[post("/<week>/vote", data="<form>")]
pub async fn vote(week: u8, form: Form<VoteForm>,
                  mut db: Connection<database::Db>, cookies: &CookieJar<'_>) -> Redirect {
    let user = auth::get_user(week, &mut db, cookies).await;

    if !user.logged_in {
        cookies.add(("toast_error", "Vous n'avez pas la permission de changer de vote."));
        return Redirect::to(uri!("/"));
    }

    let filtered_votes = form.truth_votes.iter().filter_map(
        |vote| {
            if *vote.1 != 0 {
                Some(vote)
            } else {
                None
            }
        }
    );

    let mut had_error = false;
    for (truth_id, voted_id) in filtered_votes {
        match user.votes.iter().find(|vote: &&Vote| {vote.truth_id == *truth_id}) {
            Some(vote) => {
                if *voted_id == vote.voted_id {
                    continue;
                }
                debug!("Player {:?} updating vote {:?} for truth {truth_id} : \n\t\
                    Previously voted {:?}, now voted {voted_id}", user.id, vote.id, vote.voted_id);
                match sqlx::query("UPDATE Votes SET voted_id = $3 WHERE truth_id == $1 AND voter_id == $2;")
                    .bind(truth_id)
                    .bind(user.id)
                    .bind(voted_id)
                    .fetch_optional(&mut **db)
                    .await {
                    Ok(_) => {}
                    Err(error) => {
                        error!("Error while submitting a vote : {error}");
                        had_error = true;
                    }
                };
            }
            None => {
                debug!("Player {:?} voting {voted_id} for truth {truth_id}", user.id);
                // TODO: Find a way to use only one statement ? --> query_many
                // Cannot all launch and await because all connect to DB
                match sqlx::query("INSERT INTO Votes (truth_id, voter_id, voted_id) VALUES ($1, $2, $3);")
                    .bind(truth_id)
                    .bind(user.id)
                    .bind(voted_id)
                    .fetch_optional(&mut **db)
                    .await {
                    Ok(_) => {}
                    Err(error) => {
                        error!("Error while submitting a vote : {error}");
                        had_error = true;
                    }
                };
            }
        }
    }
    if had_error {
        cookies.add(("toast_error", "Il y a eu un problème lors de la soumission du vote !"));
    } else {
        debug!("Vote successful")
    }

    Redirect::to(uri!("/"))
}

#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct VoteData {
    votes: HashMap<String, Vec<u8>>,
}

// TODO: Cache vote count ? Maintain in state ?
#[get("/<week>/votes", format = "application/json")]
pub async fn fetch_vote_data(week: u8, mut db: Connection<database::Db>) -> Option<Json<VoteData>> {
    let raw_votes: Vec<VotingData> = sqlx::query_as("
        SELECT Players.name as votes_for, Truths.number as truth_number, count(*) as votes FROM Votes
            JOIN Players ON Votes.voted_id == Players.id
            JOIN Truths on Votes.truth_id == Truths.id AND Truths.week == $1
            GROUP BY votes_for, truth_number
            ORDER BY votes_for, truth_number;")
        .bind(week)
        .fetch_all(&mut **db)
        .await.unwrap_or_else(|error| {
            error!("Error while fetching vote data {error}");
            Vec::<VotingData>::new()
    });

    let vote_count = raw_votes.iter().fold(0, |count, votes| {count + votes.votes});
    // Only show the graph if we have all the votes and this is not the last week.
    // FIXME: Make the 42 not hardcoded
    if vote_count < 42 || week != sqlx::query_scalar("SELECT number from Weeks WHERE number == $1 AND is_last_week == 1;")
        .bind(week)
        .fetch_optional(&mut **db)
        .await.unwrap_or(Some(0)).unwrap_or(0)  {
        return None;
    }


    // The hash map makes storing and retrieving the data really easy, *but*
    // it does lose the order of the original array (which is sorted via the SQL).
    let mut vote_data = HashMap::<String, Vec<u8>>::new();
    let mut next_truth_number = 1;
    for raw_vote in raw_votes {
        // The truth_number is monotonous (sorted by the SQL request).
        // If the next vote's truth is lower, we have changed player.
        if next_truth_number > raw_vote.truth_number {
            next_truth_number = 1;
        }

        let votes_for_player: &mut Vec<u8> = match vote_data.entry(raw_vote.votes_for) {
            Entry::Occupied(existing) => {existing.into_mut()}
            Entry::Vacant(vacant) => {
                next_truth_number = 1; // We changed user, reset to 1.
                vacant.insert(Vec::<u8>::new())}
        };

        // Fill up missing spaces if we are missing any.
        for _ in next_truth_number..raw_vote.truth_number {
            votes_for_player.push(0);
        }
        // Update with the vote result.
        votes_for_player.push(raw_vote.votes);
        next_truth_number = raw_vote.truth_number + 1;
    }

    Some(Json(VoteData{votes: vote_data}))
}

// FIXME:
pub fn stage() -> AdHoc {
    AdHoc::on_ignite("SQLx Stage", |rocket| async {
        rocket.mount("/", routes![vote, fetch_vote_data])
    })
}