use cosimo::{Datum, DatumKey};
use std::collections::{HashMap, HashSet};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum StudyDirection {
    Forward,
    Reverse,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StudyItem {
    deck_index: usize,
    direction: StudyDirection,
    datum_key: DatumKey,
    datum: Datum,
}

impl StudyItem {
    pub(crate) fn forward(deck_index: usize, datum: &Datum) -> Self {
        Self {
            deck_index,
            direction: StudyDirection::Forward,
            datum_key: DatumKey::for_datum(datum),
            datum: datum.clone(),
        }
    }

    pub(crate) fn reverse(deck_index: usize, datum: &Datum) -> Self {
        let parent_key = DatumKey::for_datum(datum);
        Self {
            deck_index,
            direction: StudyDirection::Reverse,
            datum_key: DatumKey::for_reverse_of(&parent_key),
            datum: Datum::new(datum.response(), datum.prompt())
                .expect("deck data has non-empty prompt and response"),
        }
    }

    pub(crate) fn deck_index(&self) -> usize {
        self.deck_index
    }

    #[cfg(test)]
    pub(crate) fn direction(&self) -> StudyDirection {
        self.direction
    }

    pub(crate) fn datum_key(&self) -> &DatumKey {
        &self.datum_key
    }

    pub(crate) fn datum(&self) -> &Datum {
        &self.datum
    }

    pub(crate) fn is_reverse(&self) -> bool {
        self.direction == StudyDirection::Reverse
    }

    pub(crate) fn parent_datum_key(&self) -> DatumKey {
        if let Some(parent_key) = self.datum_key.parent_key_for_reverse() {
            parent_key
        } else {
            self.datum_key.clone()
        }
    }
}

pub(crate) fn study_items_for_deck(
    data: &[Datum],
    reversible_keys: &HashSet<DatumKey>,
) -> Vec<StudyItem> {
    let mut items = Vec::with_capacity(data.len() + reversible_keys.len());
    for (deck_index, datum) in data.iter().enumerate() {
        items.push(StudyItem::forward(deck_index, datum));
        if reversible_keys.contains(&DatumKey::for_datum(datum)) && !datum.is_reversal_prohibited()
        {
            items.push(StudyItem::reverse(deck_index, datum));
        }
    }
    items
}

pub(crate) fn normalized_reverse_prompt(value: &str) -> String {
    value
        .replace("\r\n", "\n")
        .replace('\r', "\n")
        .trim()
        .to_string()
}

pub(crate) fn reversible_candidate_indices(
    data: &[Datum],
    reversible_keys: &HashSet<DatumKey>,
    excluded_keys: &HashSet<DatumKey>,
) -> Vec<usize> {
    let mut response_counts: HashMap<String, usize> = HashMap::new();
    for datum in data {
        *response_counts
            .entry(normalized_reverse_prompt(datum.response()))
            .or_default() += 1;
    }

    data.iter()
        .enumerate()
        .filter_map(|(index, datum)| {
            let datum_key = DatumKey::for_datum(datum);
            let response = normalized_reverse_prompt(datum.response());
            (!reversible_keys.contains(&datum_key)
                && !excluded_keys.contains(&datum_key)
                && response_counts.get(&response).copied() == Some(1))
            .then_some(index)
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    fn datum(prompt: &str, response: &str) -> Datum {
        Datum::new(prompt, response).unwrap()
    }

    #[test]
    fn reverse_study_item_swaps_prompt_and_response_and_uses_child_key() {
        let card = datum("one", "uno").with_card_id(12);
        let item = StudyItem::reverse(3, &card);

        assert_eq!(item.deck_index(), 3);
        assert_eq!(item.direction(), StudyDirection::Reverse);
        assert_eq!(item.datum().prompt(), "uno");
        assert_eq!(item.datum().response(), "one");
        assert_eq!(item.datum_key().as_str(), "card:12:reverse");
        assert_eq!(item.parent_datum_key(), DatumKey::for_card_id(12));
    }

    #[test]
    fn study_items_include_generated_reverse_after_parent() {
        let data = vec![
            datum("one", "uno").with_card_id(1),
            datum("two", "dos").with_card_id(2),
        ];
        let reversible = HashSet::from([DatumKey::for_card_id(1)]);

        let items = study_items_for_deck(&data, &reversible);

        assert_eq!(items.len(), 3);
        assert_eq!(items[0].direction(), StudyDirection::Forward);
        assert_eq!(items[1].direction(), StudyDirection::Reverse);
        assert_eq!(items[2].direction(), StudyDirection::Forward);
    }

    #[test]
    fn study_items_do_not_generate_reverse_for_non_reversible_cards() {
        let data = vec![
            datum("one", "uno")
                .with_card_id(1)
                .with_reversal_prohibited(true),
        ];
        let reversible = HashSet::from([DatumKey::for_card_id(1)]);

        let items = study_items_for_deck(&data, &reversible);

        assert_eq!(items.len(), 1);
        assert_eq!(items[0].direction(), StudyDirection::Forward);
        assert_eq!(items[0].datum().prompt(), "one");
    }

    #[test]
    fn reversible_candidates_exclude_existing_reversals_duplicate_responses_and_exclusions() {
        let data = vec![
            datum("one", "uno").with_card_id(1),
            datum("another one", " uno\n").with_card_id(2),
            datum("two", "dos").with_card_id(3),
            datum("three", "tres").with_card_id(4),
            datum("four", "cuatro").with_card_id(5),
        ];
        let reversible = HashSet::from([DatumKey::for_card_id(3)]);
        let excluded = HashSet::from([DatumKey::for_card_id(4)]);

        assert_eq!(
            reversible_candidate_indices(&data, &reversible, &excluded),
            vec![4]
        );
    }
}
