use std::{collections::HashMap, str::FromStr};

use fraction::Fraction;

use crate::{
    subcommands::{turn_grade_type_name_to_grade_type, turn_string_to_grade_type_group},
    types::{
        config::GradesConfigFile,
        grade::Grade,
        grade_type::{GradeType, GradeTypeName},
    },
};

#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct GroupAverage {
    pub grade_type: GradeType,
    pub average: Fraction,
    pub weighted_average: Fraction,
    pub weight: Fraction,
}

impl From<GradeAverage> for GroupAverage {
    fn from(value: GradeAverage) -> Self {
        let average = value
            .average
            .iter()
            .fold(Fraction::from(0), |acc, value| acc + *value)
            / value.average.len();
        GroupAverage {
            grade_type: value.grade_type,
            average,
            weighted_average: average * value.weight,
            weight: value.weight,
        }
    }
}

#[derive(Debug, Clone)]
pub struct GradeAverage {
    pub grade_type: GradeType,
    pub average: Vec<Fraction>,
    pub weight: Fraction,
}

#[derive(Debug)]
pub struct ClassAverage {
    pub averages: Vec<GroupAverage>,
    //pub sum_of_weights: Fraction,
}

impl ClassAverage {
    pub fn get_total_average(&self) -> Fraction {
        self.averages
            .iter()
            .map(|value| value.weighted_average)
            .fold(Fraction::from(0), |acc, value| acc + value)
            / self
                .averages
                .iter()
                .fold(Fraction::from(0), |acc, value| acc + value.weight)
    }
    pub fn generate_from_data(
        config_file: &GradesConfigFile,
        grade_type_name_hash: &HashMap<GradeTypeName, Vec<Grade>>,
    ) -> ClassAverage {
        let mut averages: Vec<GradeAverage> = vec![];
        let mut result_averages = ClassAverage { averages: vec![] };

        grade_type_name_hash.iter().for_each(|(key, value)| {
            let grade_type = turn_grade_type_name_to_grade_type(key, config_file);
            let average = Fraction::from(value.iter().fold(0, |acc, value| acc + value.value))
                / Fraction::from(value.len());
            let weight = Fraction::from_str(
                &turn_string_to_grade_type_group(&grade_type.group.0, config_file)
                    .weight
                    .0,
            )
            .expect("We checked this value in the config check");

            let grade_types_average = GradeAverage {
                grade_type,
                average: vec![average],
                weight,
            };

            //averages.sum_of_weights += grade_types_average.weight;
            averages.push(grade_types_average);
        });

        averages.sort_by_key(|average| average.grade_type.group.clone());
        averages[..]
            .group_by(|x, y| x.grade_type.group == y.grade_type.group)
            .for_each(|slice| {
                let first_average = if let Some(output) = slice.get(0) {
                    output
                } else {
                    unreachable!();
                };
                result_averages.averages.push(GroupAverage::from(
                    slice
                        .iter()
                        .skip(1)
                        .fold(first_average.clone(), |acc, value| {
                            value.merge_with_other(acc)
                        }),
                ))
            });
        result_averages
    }
}

impl GradeAverage {
    pub fn merge_with_other(&self, mut other: GradeAverage) -> GradeAverage {
        // To prevent undefined behavior
        // TODO this NEEDS to pass:
        //assert_eq!(self.grade_type, other.grade_type);
        assert_eq!(self.grade_type.group, other.grade_type.group);

        assert_eq!(self.weight, other.weight);

        let mut average = self.average.clone();
        average.append(&mut other.average);

        GradeAverage {
            grade_type: self.grade_type.clone(),
            average,
            weight: self.weight,
        }
    }
}

#[cfg(test)]
mod test {
    use crate::types::grade_type::{GradeType, GradeTypeGroupName, GradeTypeName};

    use super::{GradeAverage, GroupAverage};

    use pretty_assertions::assert_eq;

    #[test]
    fn averages_without_weights() {
        let average1 = GradeAverage {
            grade_type: GradeType {
                name: GradeTypeName("TEST1".to_owned()),
                group: GradeTypeGroupName("GROUP_TEST1".to_owned()),
            },
            average: vec![2.into(), 3.into()],
            weight: 1.into(),
        };

        let average2 = GradeAverage {
            grade_type: GradeType {
                name: GradeTypeName("TEST2".to_owned()),
                group: GradeTypeGroupName("GROUP_TEST1".to_owned()),
            },
            average: vec![1.into(), 2.into()],
            weight: 1.into(),
        };

        let result = GroupAverage {
            grade_type: GradeType {
                // TODO change, so that TEST2 doesn't get overwritten
                name: GradeTypeName("TEST1".to_owned()),
                group: GradeTypeGroupName("GROUP_TEST1".to_owned()),
            },
            average: 2.into(),
            weighted_average: 2.into(),
            weight: 1.into(),
        };

        assert_eq!(result, average1.merge_with_other(average2).into())
    }
}
