use crate::{
    db, deck,
    model::{DbEntry, Line, Question},
};
use anyhow::Result;
use rusqlite::Connection;
use std::collections::HashMap;
use std::collections::HashSet;

pub fn run(conn: &mut Connection, deck_path: &str) -> Result<()> {
    let db_entries = db::cards::all(conn)?;
    let lines = deck::read_file(deck_path)?;
    let Diff {
        new,
        deleted,
        undeleted,
    } = diff(db_entries, lines);

    db::cards::insert(conn, &new)?;
    db::cards::delete(conn, &deleted)?;
    db::cards::undelete(conn, &undeleted)?;

    Ok(())
}

struct Diff {
    pub new: Vec<Question>,
    pub deleted: Vec<Question>,
    pub undeleted: Vec<Question>,
}

fn diff(db_entries: Vec<DbEntry>, lines: Vec<Line>) -> Diff {
    let mut file_questions = HashMap::<String, Vec<String>>::new();
    let mut db_questions_not_deleted = HashSet::<Question>::new();
    let mut db_questions_deleted = HashSet::<Question>::new();

    for Line { part_1, part_2 } in lines {
        insert(&mut file_questions, part_1.clone(), part_2.clone());
        insert(&mut file_questions, part_2, part_1);
    }

    let file_questions: HashSet<Question> = file_questions
        .iter()
        .map(|(question, responses)| Question {
            question: question.to_string(),
            responses: responses.to_vec(),
        })
        .collect();

    for DbEntry {
        question,
        mut responses,
        deleted,
    } in db_entries
    {
        responses.sort();
        if deleted.is_some() {
            db_questions_deleted.insert(Question {
                question,
                responses,
            });
        } else {
            db_questions_not_deleted.insert(Question {
                question,
                responses,
            });
        }
    }

    let new = file_questions
        .difference(&db_questions_not_deleted)
        .cloned()
        .collect::<HashSet<Question>>()
        .difference(&db_questions_deleted)
        .cloned()
        .collect();

    let deleted = db_questions_not_deleted
        .difference(&file_questions)
        .cloned()
        .collect();

    let undeleted = file_questions
        .intersection(&db_questions_deleted)
        .cloned()
        .collect();

    Diff {
        new,
        deleted,
        undeleted,
    }
}

fn insert(map: &mut HashMap<String, Vec<String>>, questions: Vec<String>, responses: Vec<String>) {
    for question in questions {
        let mut responses = responses.clone();
        responses.sort();
        match map.get_mut(&question) {
            Some(existing_responses) => existing_responses.append(&mut responses),
            None => {
                map.insert(question, responses);
            }
        };
    }
}

#[cfg(test)]
mod tests {

    use super::{deck, DbEntry, Diff, Question};
    use std::collections::HashSet;

    #[test]
    fn test_added() {
        let diff = deck_diff("A : a", "A : a\nB : b");

        has_questions(diff.new, vec![("B", vec!["b"]), ("b", vec!["B"])]);
        assert!(diff.deleted.is_empty());
        assert!(diff.undeleted.is_empty());
    }

    #[test]
    fn test_updated() {
        let diff = deck_diff("A : a1", "A : a2");

        has_questions(diff.new, vec![("A", vec!["a2"]), ("a2", vec!["A"])]);
        has_questions(diff.deleted, vec![("A", vec!["a1"]), ("a1", vec!["A"])]);
        assert!(diff.undeleted.is_empty());
    }

    #[test]
    fn test_deleted() {
        let diff = deck_diff("A : a", "");

        assert!(diff.new.is_empty());
        has_questions(diff.deleted, vec![("A", vec!["a"]), ("a", vec!["A"])]);
        assert!(diff.undeleted.is_empty());
    }

    #[test]
    fn test_undeleted() {
        let db_entries = vec![DbEntry {
            question: "A".to_string(),
            responses: vec!["a".to_string()],
            deleted: Some(0),
        }];

        let diff = super::diff(db_entries, deck::tests::read_string("A : a").unwrap());

        has_questions(diff.new, vec![("a", vec!["A"])]);
        assert!(diff.deleted.is_empty());
        has_questions(diff.undeleted, vec![("A", vec!["a"])]);
    }
    #[test]
    fn regroup_same_question() {
        let diff = deck_diff("", "A : a\nA | B : b");

        has_questions(
            diff.new,
            vec![
                ("A", vec!["a", "b"]),
                ("B", vec!["b"]),
                ("a", vec!["A"]),
                ("b", vec!["A", "B"]),
            ],
        );
        assert!(diff.deleted.is_empty());
        assert!(diff.undeleted.is_empty());
    }

    fn deck_diff(from: &str, to: &str) -> Diff {
        super::diff(db_entries(from), deck::tests::read_string(to).unwrap())
    }

    fn has_questions(questions: Vec<Question>, xs: Vec<(&str, Vec<&str>)>) {
        assert_eq!(
            to_set(questions),
            HashSet::from_iter(
                xs.iter()
                    .map(|(y, ys)| Question {
                        question: y.to_string(),
                        responses: ys.iter().map(|z| z.to_string()).collect::<Vec<_>>()
                    })
                    .collect::<Vec<_>>()
            )
        )
    }

    fn to_set<A: std::cmp::Eq + std::hash::Hash + std::clone::Clone>(xs: Vec<A>) -> HashSet<A> {
        xs.iter().cloned().collect()
    }

    fn db_entries(deck: &str) -> Vec<DbEntry> {
        let lines = deck::tests::read_string(deck).unwrap();
        let diff = super::diff(vec![], lines);
        diff.new
            .iter()
            .map(
                |Question {
                     question,
                     responses,
                 }| DbEntry {
                    question: question.to_string(),
                    responses: responses.to_vec(),
                    deleted: None,
                },
            )
            .collect()
    }
}