use crate::localization::{CycleSummaryText, Localizer};
use cosimo::{PromptGuess, RecallFeedback, Review, ReviewPassItem};

#[derive(Debug, Clone, Default)]
pub(crate) enum ReviewSession {
    #[default]
    Ready,
    Running(Box<RunningReviewSession>),
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReviewSessionKind {
    ScheduledStudy,
    UnscheduledStudy,
    Exam,
    Quiz,
}

impl ReviewSessionKind {
    #[cfg(test)]
    fn for_study_session(was_scheduled: bool) -> Self {
        if was_scheduled {
            Self::ScheduledStudy
        } else {
            Self::UnscheduledStudy
        }
    }

    fn was_scheduled_study_session(self) -> bool {
        self == Self::ScheduledStudy
    }
}

impl ReviewSession {
    #[cfg(test)]
    pub(crate) fn start(
        queue: Vec<usize>,
        deck_len: usize,
        was_scheduled: bool,
        _repeat_seed: u64,
        started_at_unix_seconds: i64,
    ) -> Result<Self, EmptyReviewQueue> {
        Self::start_with_kind(
            queue,
            deck_len,
            ReviewSessionKind::for_study_session(was_scheduled),
            _repeat_seed,
            started_at_unix_seconds,
        )
    }

    pub(crate) fn start_with_kind(
        queue: Vec<usize>,
        deck_len: usize,
        kind: ReviewSessionKind,
        _repeat_seed: u64,
        started_at_unix_seconds: i64,
    ) -> Result<Self, EmptyReviewQueue> {
        Ok(Self::Running(Box::new(RunningReviewSession::new(
            queue,
            deck_len,
            kind,
            started_at_unix_seconds,
        )?)))
    }

    pub(crate) fn is_ready(&self) -> bool {
        matches!(self, Self::Ready)
    }

    pub(crate) fn phase(&self) -> ReviewPhase {
        match self {
            Self::Ready => ReviewPhase::ReadyToStart,
            Self::Running(session) => session.phase(),
        }
    }

    pub(crate) fn queue(&self) -> &[usize] {
        match self {
            Self::Ready => &[],
            Self::Running(session) => session.queue(),
        }
    }

    pub(crate) fn scheduled_card_count(&self) -> usize {
        self.queue().len()
    }

    pub(crate) fn reviewed_card_count(&self) -> usize {
        match self {
            Self::Ready => 0,
            Self::Running(session) => session.reviewed_card_count(),
        }
    }

    pub(crate) fn kind(&self) -> Option<ReviewSessionKind> {
        match self {
            Self::Ready => None,
            Self::Running(session) => Some(session.kind()),
        }
    }

    #[cfg(test)]
    pub(crate) fn stats(&self) -> ReviewCycleStats {
        match self {
            Self::Ready => ReviewCycleStats::default(),
            Self::Running(session) => session.stats(),
        }
    }

    pub(crate) fn current_index(&self) -> usize {
        match self {
            Self::Ready => 0,
            Self::Running(session) => session.current_index(),
        }
    }

    pub(crate) fn current_deck_index(&self) -> Option<usize> {
        match self {
            Self::Ready => None,
            Self::Running(session) => Some(session.current_deck_index()),
        }
    }

    pub(crate) fn show_response(&mut self, guess: PromptGuess) -> bool {
        let Self::Running(session) = self else {
            return false;
        };
        session.show_response(guess);
        true
    }

    pub(crate) fn response_guess(&self) -> Option<PromptGuess> {
        let Self::Running(session) = self else {
            return None;
        };
        session.response_guess()
    }

    pub(crate) fn record_completed_review(
        &mut self,
        review: Review,
        feedback: RecallFeedback,
        review_event_id: i64,
        completed_at_unix_seconds: i64,
    ) -> Option<ReviewProgress> {
        let Self::Running(session) = self else {
            return None;
        };
        Some(session.record_completed_review(
            review,
            feedback,
            review_event_id,
            completed_at_unix_seconds,
        ))
    }

    pub(crate) fn defer_current_prompt(
        &mut self,
        deferred_at_unix_seconds: i64,
    ) -> Option<ReviewDeferralProgress> {
        let Self::Running(session) = self else {
            return None;
        };
        let progress = session.defer_current_prompt(deferred_at_unix_seconds)?;
        if progress.pass_finished() {
            *self = Self::Ready;
        }
        Some(progress)
    }

    pub(crate) fn end_current_pass(
        &mut self,
        ended_at_unix_seconds: i64,
    ) -> Option<ReviewCycleSummary> {
        let Self::Running(session) = self else {
            return None;
        };
        let summary = session.early_summary(ended_at_unix_seconds);
        *self = Self::Ready;
        Some(summary)
    }

    pub(crate) fn reset(&mut self) {
        *self = Self::Ready;
    }
}

#[derive(Debug, Clone)]
pub(crate) struct RunningReviewSession {
    // Running sessions always have at least one queued card. Keeping that
    // invariant in ReviewQueue lets the review flow index the current card
    // directly without scattering empty-queue checks through the UI handlers.
    queue: ReviewQueue,
    initial_queue_len: usize,
    reviewed_cards_seen: Vec<bool>,
    review_items: Vec<ReviewPassItem>,
    started_at_unix_seconds: i64,
    phase: RunningReviewPhase,
    stats: ReviewCycleStats,
    kind: ReviewSessionKind,
}

impl RunningReviewSession {
    fn new(
        queue: Vec<usize>,
        deck_len: usize,
        kind: ReviewSessionKind,
        started_at_unix_seconds: i64,
    ) -> Result<Self, EmptyReviewQueue> {
        let initial_queue_len = queue.len();
        Ok(Self {
            queue: ReviewQueue::new(queue)?,
            initial_queue_len,
            reviewed_cards_seen: vec![false; deck_len],
            review_items: Vec::new(),
            started_at_unix_seconds,
            phase: RunningReviewPhase::Prompt,
            stats: ReviewCycleStats::default(),
            kind,
        })
    }

    fn phase(&self) -> ReviewPhase {
        match self.phase {
            RunningReviewPhase::Prompt => ReviewPhase::Prompt,
            RunningReviewPhase::Response { guess } => ReviewPhase::Response { guess },
        }
    }

    fn queue(&self) -> &[usize] {
        self.queue.as_slice()
    }

    fn current_index(&self) -> usize {
        self.queue.current_index()
    }

    #[cfg(test)]
    fn stats(&self) -> ReviewCycleStats {
        self.stats
    }

    fn reviewed_card_count(&self) -> usize {
        self.stats.reviewed_cards
    }

    fn kind(&self) -> ReviewSessionKind {
        self.kind
    }

    fn current_deck_index(&self) -> usize {
        self.queue.current_deck_index()
    }

    fn show_response(&mut self, guess: PromptGuess) {
        self.phase = RunningReviewPhase::Response { guess };
    }

    fn response_guess(&self) -> Option<PromptGuess> {
        match self.phase {
            RunningReviewPhase::Response { guess } => Some(guess),
            RunningReviewPhase::Prompt => None,
        }
    }

    fn record_completed_review(
        &mut self,
        review: Review,
        _feedback: RecallFeedback,
        review_event_id: i64,
        completed_at_unix_seconds: i64,
    ) -> ReviewProgress {
        let deck_index = self.current_deck_index();
        let completed_index = self.queue.completed_index();
        let is_unique_card = self.mark_card_reviewed(deck_index);
        self.stats.record(review, is_unique_card);
        self.review_items.push(ReviewPassItem::new(
            review_event_id,
            completed_index,
            !is_unique_card,
        ));
        let total_cards = self.queue.len();
        let finished_cycle = completed_index == total_cards;
        self.queue.advance();
        self.phase = RunningReviewPhase::Prompt;

        ReviewProgress {
            completed_index,
            total_cards,
            completed_summary: finished_cycle.then_some(ReviewCycleSummary {
                stats: self.stats,
                total_cards,
                initial_cards: self.initial_queue_len,
                started_at_unix_seconds: self.started_at_unix_seconds,
                finished_at_unix_seconds: self
                    .finished_at_unix_seconds_at(completed_at_unix_seconds),
                elapsed_seconds: self.elapsed_seconds_at(completed_at_unix_seconds),
                ended_early: false,
                was_scheduled: self.kind.was_scheduled_study_session(),
                items: self.review_items.clone(),
                warning: None,
            }),
        }
    }

    fn defer_current_prompt(
        &mut self,
        deferred_at_unix_seconds: i64,
    ) -> Option<ReviewDeferralProgress> {
        if self.phase != RunningReviewPhase::Prompt {
            return None;
        }

        let removed_from_initial_queue = self.queue.current_index() < self.initial_queue_len;
        let removed_last_queued_card = self.queue.remove_current();
        if removed_from_initial_queue {
            self.initial_queue_len = self.initial_queue_len.saturating_sub(1);
        }

        if self.queue.is_empty() || removed_last_queued_card {
            let completed_summary = (self.stats.reviewed_cards > 0).then(|| ReviewCycleSummary {
                stats: self.stats,
                total_cards: self.queue.len(),
                initial_cards: self.initial_queue_len,
                started_at_unix_seconds: self.started_at_unix_seconds,
                finished_at_unix_seconds: self
                    .finished_at_unix_seconds_at(deferred_at_unix_seconds),
                elapsed_seconds: self.elapsed_seconds_at(deferred_at_unix_seconds),
                ended_early: false,
                was_scheduled: self.kind.was_scheduled_study_session(),
                items: self.review_items.clone(),
                warning: None,
            });
            return Some(ReviewDeferralProgress::Finished { completed_summary });
        }

        Some(ReviewDeferralProgress::Continuing)
    }

    fn mark_card_reviewed(&mut self, deck_index: usize) -> bool {
        let Some(already_seen) = self.reviewed_cards_seen.get_mut(deck_index) else {
            return false;
        };
        if *already_seen {
            false
        } else {
            *already_seen = true;
            true
        }
    }

    fn early_summary(&self, ended_at_unix_seconds: i64) -> ReviewCycleSummary {
        ReviewCycleSummary {
            stats: self.stats,
            total_cards: self.queue.len(),
            initial_cards: self.initial_queue_len,
            started_at_unix_seconds: self.started_at_unix_seconds,
            finished_at_unix_seconds: self.finished_at_unix_seconds_at(ended_at_unix_seconds),
            elapsed_seconds: self.elapsed_seconds_at(ended_at_unix_seconds),
            ended_early: true,
            was_scheduled: self.kind.was_scheduled_study_session(),
            items: self.review_items.clone(),
            warning: None,
        }
    }

    fn elapsed_seconds_at(&self, ended_at_unix_seconds: i64) -> u64 {
        self.finished_at_unix_seconds_at(ended_at_unix_seconds)
            .saturating_sub(self.started_at_unix_seconds) as u64
    }

    fn finished_at_unix_seconds_at(&self, ended_at_unix_seconds: i64) -> i64 {
        ended_at_unix_seconds.max(self.started_at_unix_seconds)
    }
}

#[derive(Debug, Clone)]
struct ReviewQueue {
    cards: Vec<usize>,
    current_index: usize,
}

impl ReviewQueue {
    fn new(cards: Vec<usize>) -> Result<Self, EmptyReviewQueue> {
        if cards.is_empty() {
            return Err(EmptyReviewQueue);
        }

        Ok(Self {
            cards,
            current_index: 0,
        })
    }

    fn as_slice(&self) -> &[usize] {
        &self.cards
    }

    fn len(&self) -> usize {
        self.cards.len()
    }

    fn is_empty(&self) -> bool {
        self.cards.is_empty()
    }

    fn current_index(&self) -> usize {
        self.current_index
    }

    fn completed_index(&self) -> usize {
        self.current_index + 1
    }

    fn current_deck_index(&self) -> usize {
        self.cards[self.current_index]
    }

    fn remove_current(&mut self) -> bool {
        let removed_index = self.current_index;
        self.cards.remove(removed_index);
        removed_index >= self.cards.len()
    }

    fn advance(&mut self) {
        self.current_index = (self.current_index + 1) % self.cards.len();
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RunningReviewPhase {
    Prompt,
    Response { guess: PromptGuess },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReviewPhase {
    ReadyToStart,
    Prompt,
    Response { guess: PromptGuess },
}

#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct ReviewCycleStats {
    pub(crate) reviewed_cards: usize,
    pub(crate) unique_reviewed_cards: usize,
    pub(crate) prompt_guess_counts: [usize; PromptGuess::COUNT],
    pub(crate) recall_feedback_counts: [usize; RecallFeedback::COUNT],
}

impl ReviewCycleStats {
    pub(crate) fn record(&mut self, review: Review, is_unique_card: bool) {
        self.reviewed_cards += 1;
        if is_unique_card {
            self.unique_reviewed_cards += 1;
        }
        self.prompt_guess_counts[review.prompt_guess().index()] += 1;
        self.recall_feedback_counts[review.recall_feedback().index()] += 1;
    }

    pub(crate) fn summary_text(
        &self,
        localizer: Localizer,
        total_cards: usize,
        elapsed_seconds: u64,
        ended_early: bool,
        was_scheduled: bool,
    ) -> String {
        localizer.cycle_summary_text(CycleSummaryText {
            reviewed_cards: self.reviewed_cards,
            unique_reviewed_cards: self.unique_reviewed_cards,
            total_cards,
            elapsed_seconds,
            ended_early,
            was_scheduled,
            prompt_guess_counts: self.prompt_guess_counts,
            recall_feedback_counts: self.recall_feedback_counts,
        })
    }
}

#[derive(Debug, Clone)]
pub(crate) struct ReviewCycleSummary {
    pub(crate) stats: ReviewCycleStats,
    pub(crate) total_cards: usize,
    pub(crate) initial_cards: usize,
    pub(crate) started_at_unix_seconds: i64,
    pub(crate) finished_at_unix_seconds: i64,
    pub(crate) elapsed_seconds: u64,
    pub(crate) ended_early: bool,
    pub(crate) was_scheduled: bool,
    pub(crate) items: Vec<ReviewPassItem>,
    pub(crate) warning: Option<String>,
}

#[derive(Debug, Clone)]
pub(crate) struct ReviewProgress {
    pub(crate) completed_index: usize,
    pub(crate) total_cards: usize,
    pub(crate) completed_summary: Option<ReviewCycleSummary>,
}

#[derive(Debug, Clone)]
pub(crate) enum ReviewDeferralProgress {
    Continuing,
    Finished {
        completed_summary: Option<ReviewCycleSummary>,
    },
}

impl ReviewDeferralProgress {
    pub(crate) fn pass_finished(&self) -> bool {
        matches!(self, Self::Finished { .. })
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct EmptyReviewQueue;

#[cfg(test)]
pub(crate) fn format_count_and_percentage(count: usize, total: usize) -> String {
    if total == 0 {
        return format!("{count} (0.0%)");
    }

    let percentage = (count as f64 / total as f64) * 100.0;
    format!("{count} ({percentage:.1}%)")
}
