use std::{collections::HashMap, path::PathBuf, process};

use fraction::Fraction;

use crate::{
    command_line_interface::ListType,
    subcommands::list::average::ClassAverage,
    types::{class::Class, data::GradesDataFile, grade::Grade, grade_type::GradeTypeName},
};

use super::{deserialize_config_and_data, turn_string_to_class};

pub mod average;

pub fn list(
    class: &Option<String>,
    mode: &ListType,
    raw_data_file: &PathBuf,
    raw_config_file: &PathBuf,
) {
    let (data_file, config_file) = deserialize_config_and_data(raw_data_file, raw_config_file);
    if let Some(class) = class {
        let class = turn_string_to_class(class, &config_file);
        let grade_type_name_hash: &HashMap<GradeTypeName, Vec<Grade>> =
            match data_file.classes.get(&class.class_type) {
                Some(hash) => match hash.get(&class.name) {
                    Some(hash) => hash,
                    None => {
                        eprintln!(
                            "Add grades to the class {} first, there are none to list",
                            class.name
                        );
                        process::exit(1)
                    }
                },
                None => {
                    eprintln!(
                        "Add grades to the class_type {} first, there are none to list",
                        class.class_type
                    );
                    process::exit(1)
                }
            };
        match mode {
            ListType::Best => {
                worst_and_best_grades_for_one_classes(true, grade_type_name_hash, class);
            }
            ListType::Worst => {
                worst_and_best_grades_for_one_classes(false, grade_type_name_hash, class);
            }
            ListType::Average => {
                let averages = ClassAverage::generate_from_data(&config_file, grade_type_name_hash);
                averages.averages.iter().for_each(|average| {
                    print!("{}: ", average.grade_type.group);
                    println!(
                        "{:.2} {}",
                        average.average,
                        config_file.unit // TODO take the unit from a grade
                    );
                });

                // TODO take the unit from a grade, and not the config file
                println!(
                    "\nTotal: {:.2} {}",
                    averages.get_total_average(),
                    config_file.unit
                );
            }
            ListType::All => {
                list_all_grades(grade_type_name_hash);
            }
        };
    } else {
        match mode {
            ListType::Best => {
                worst_and_best_grades_for_all_classes(true, data_file);
            }
            ListType::Worst => {
                worst_and_best_grades_for_all_classes(false, data_file);
            }
            ListType::Average => {
                let mut total_averages = vec![];
                data_file.classes.iter().for_each(|(_, value)| {
                    value.iter().for_each(|(_, value)| {
                        total_averages.push(
                            ClassAverage::generate_from_data(&config_file, value)
                                .get_total_average(),
                        )
                    });
                });
                println!(
                    "Average: {:.2} {}",
                    total_averages
                        .iter()
                        .fold(Fraction::from(0), |acc, value| acc + *value)
                        / total_averages.len(),
                    config_file.unit
                )
            }
            ListType::All => data_file.classes.values().for_each(|value| {
                value.iter().for_each(|(key, value)| {
                    println!("***{key}***");
                    list_all_grades(value);
                })
            }),
        }
    }
}
fn list_all_grades(grade_type_name_hash: &HashMap<GradeTypeName, Vec<Grade>>) {
    grade_type_name_hash.iter().for_each(|(key, value)| {
        println!("Grades in {key}");
        value.iter().for_each(|grade| {
            if let Some(comment) = &grade.comment {
                println!("{} {}, ({})", grade.value, grade.unit, comment)
            } else {
                println!("{} {}", grade.value, grade.unit)
            }
        });
        println!();
    });
}
fn worst_and_best_grades_for_one_classes(
    best: bool,
    grade_type_name_hash: &HashMap<GradeTypeName, Vec<Grade>>,
    class: Class,
) {
    let grades: Vec<Grade> = grade_type_name_hash
        .iter()
        .map(|(key, value)| {
            return value.iter().fold(
                Grade {
                    grade_type: key.clone(),
                    value: 15,
                    class: class.clone(),
                    comment: None,
                    unit: "Points".into(),
                },
                |acc, value| {
                    if best {
                        if value.value >= acc.value {
                            value.clone()
                        } else {
                            acc
                        }
                    } else if value.value <= acc.value {
                        value.clone()
                    } else {
                        acc
                    }
                },
            );
        })
        .collect();
    grades.iter().for_each(|grade| {
        if best {
            println!(
                "Your best grade in {} is {} {}",
                grade.grade_type, grade.value, grade.unit
            );
        } else {
            println!(
                "Your worst grade in {} is {} {}",
                grade.grade_type, grade.value, grade.unit
            );
        }
    });
}
fn worst_and_best_grades_for_all_classes(best: bool, data_file: GradesDataFile) {
    let mut all_grades: Vec<&Grade> = data_file
        .classes
        .values()
        .flat_map(|class_name_hash| class_name_hash.values())
        .flat_map(|grade_type_name| grade_type_name.values())
        .flatten()
        .collect();
    all_grades.sort_by(|x, y| x.value.cmp(&y.value));
    if best {
        all_grades.reverse();
    }

    // TODO Rewrite, when the group_by feature becomes stable
    match all_grades[..]
        .group_by(|grade1, grade2| grade1.value == grade2.value)
        .next()
    {
        Some(grades) => {
            if grades.len() == 1 {
                //There is only one grade, we checked in the if
                let grade = grades[0];
                if best {
                    println!(
                        "Your best grade is in {}, with {} {}",
                        grade.class.name, grade.value, grade.unit
                    );
                } else {
                    println!(
                        "Your worst grade is in {}, with {} {}",
                        grade.class.name, grade.value, grade.unit
                    );
                }
            } else {
                grades.iter().for_each(|grade| {
                        if best {
                            println! {"One of your best grades is in {}, with {} {}", grade.class.name, grade.value, grade.unit};
                        } else {
                            println! {"One of your worst grades is in {}, with {} {}", grade.class.name, grade.value, grade.unit};
                        }
                        });
            }
        }
        None => {
            eprintln!("There are no grades yet, add them first");
            process::exit(1);
        }
    };
}
