use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::fs;
use std::path::{Path, PathBuf};

use cosimo::{
    CardSchedule, Datum, DatumKey, DeckMetadata, PromptGuess, RecallFeedback, Review, ReviewPass,
    ReviewPassSummary as StoredReviewPassSummary, SchedulerConfig, SqliteReviewStore,
    format_deck_with_metadata, parse_deck_file, schedule_session_for_keys,
};

use crate::app_config::{AppSettings, ReviewDbBackupPolicy};
use crate::card_details_model::{CardDetailsState, card_details_text};
use crate::deck_data::{
    deck_needs_card_ids, enumerate_deck_cards, next_unused_card_id, review_db_path_for_deck,
};
use crate::deck_properties_model::deck_properties_text;
use crate::exam_mode::{
    ExamPass, ExamQuestion, ExamSizeRequest, ExamSummary, GradedExamAnswer,
    resolve_exam_question_count,
};
use crate::file_ops::{
    apply_deck_backup_policy, backup_review_database, default_backup_dir, write_deck_file_durably,
    write_text_file_durably,
};
use crate::learning_dashboard_model::{
    LearningDashboardCard, learning_dashboard_report_with_calibration_rule, learning_dashboard_text,
};
use crate::localization::Localizer;
use crate::ready_list::{
    ReadyCardFilter, ReadyCardSort, ReadyCardSortDirection, ReadyStudyItemFilterState,
    normalize_ready_selection, ready_card_matches_text_filter, ready_study_item_indices_for_filter,
};
use crate::ready_screen::{append_status_warning, schedule_preview_if_started_now};
use crate::review_audio::ReviewAudioPlayer;
use crate::review_session::{
    ReviewCycleSummary, ReviewDeferralProgress, ReviewPhase, ReviewSession,
};
use crate::runtime::{current_time_and_seed, shuffle_indices};
use crate::study_item::{
    StudyItem, response_is_unambiguous_for_reversal, reversible_candidate_indices,
    study_items_for_deck,
};

const DEFAULT_DEFER_SECONDS: i64 = 24 * 60 * 60;

fn apply_legacy_reversal_exclusions_to_deck(
    data: &mut [Datum],
    excluded_keys: &HashSet<DatumKey>,
) -> bool {
    let mut changed = false;
    for datum in data {
        if excluded_keys.contains(&DatumKey::for_datum(datum)) && !datum.is_reversal_prohibited() {
            *datum = datum.clone().with_reversal_prohibited(true);
            changed = true;
        }
    }
    changed
}

pub(crate) struct AppState {
    // The GUI is single-deck. AppState owns the loaded deck, its neighboring
    // review database, the current review session, and the ready-list view
    // state. Event handlers mutate this object and then render controls from it.
    pub(crate) localizer: Localizer,
    pub(crate) settings: AppSettings,
    pub(crate) deck_path: PathBuf,
    pub(crate) review_db_path: PathBuf,
    pub(crate) backup_dir: PathBuf,
    pub(crate) deck_metadata: DeckMetadata,
    pub(crate) data: Vec<Datum>,
    pub(crate) session: ReviewSession,
    pub(crate) selected_ready_key: Option<DatumKey>,
    pub(crate) selected_ready_index: Option<usize>,
    pub(crate) ready_card_filter: ReadyCardFilter,
    pub(crate) ready_card_text_filter: String,
    pub(crate) ready_card_sort: ReadyCardSort,
    pub(crate) ready_card_sort_direction: ReadyCardSortDirection,
    pub(crate) ready_visible_indices: RefCell<Vec<usize>>,
    pub(crate) ready_list_sync_in_progress: Cell<bool>,
    pub(crate) last_ready_due_notification_count: Option<usize>,
    pub(crate) store: SqliteReviewStore,
    pub(crate) last_status: String,
    pub(crate) review_audio_player: RefCell<ReviewAudioPlayer>,
}

impl AppState {
    #[cfg(test)]
    pub(crate) fn load(
        deck_path: impl AsRef<Path>,
        review_db_path: impl AsRef<Path>,
    ) -> Result<Self, String> {
        let deck_path = deck_path.as_ref();
        Self::load_with_backup_dir(
            deck_path,
            review_db_path.as_ref(),
            deck_path
                .parent()
                .unwrap_or_else(|| Path::new("."))
                .join("backups"),
            Localizer::default(),
            AppSettings::default(),
        )
    }

    fn load_with_localizer(
        deck_path: impl AsRef<Path>,
        review_db_path: impl AsRef<Path>,
        localizer: Localizer,
        settings: AppSettings,
    ) -> Result<Self, String> {
        Self::load_with_backup_dir(
            deck_path,
            review_db_path,
            default_backup_dir(),
            localizer,
            settings,
        )
    }

    pub(crate) fn load_with_backup_dir(
        deck_path: impl AsRef<Path>,
        review_db_path: impl AsRef<Path>,
        backup_dir: impl AsRef<Path>,
        localizer: Localizer,
        settings: AppSettings,
    ) -> Result<Self, String> {
        let deck_path = deck_path.as_ref();
        let review_db_path = review_db_path.as_ref();
        let backup_dir = backup_dir.as_ref();
        let deck_text = fs::read_to_string(deck_path)
            .map_err(|source| localizer.could_not_read_deck_file(deck_path, &source.to_string()))?;
        let parsed_deck = parse_deck_file(&deck_text)
            .map_err(|source| localizer.could_not_parse_deck(deck_path, &source.to_string()))?;
        let mut deck_metadata = parsed_deck.metadata().clone();
        let parsed_data = parsed_deck.into_data();
        let store = SqliteReviewStore::open(review_db_path).map_err(|source| {
            localizer.could_not_open_review_database(review_db_path, &source.to_string())
        })?;
        let legacy_reversal_exclusions =
            store
                .legacy_reversal_excluded_datum_keys()
                .map_err(|source| {
                    localizer.could_not_open_review_database(review_db_path, &source.to_string())
                })?;
        let needs_deck_migration = !deck_metadata.has_header()
            || deck_needs_card_ids(&parsed_data)
            || !legacy_reversal_exclusions.is_empty();
        if needs_deck_migration {
            apply_deck_backup_policy(deck_path, backup_dir, settings.deck_backup_policy)
                .map_err(|error| localizer.could_not_write_path(deck_path, &error.to_string()))?;
        }
        let mut changed = deck_metadata.ensure_v2_header();
        let (mut data, card_id_changed) =
            enumerate_deck_cards(parsed_data, &store).map_err(|source| {
                localizer.could_not_open_review_database(review_db_path, &source.to_string())
            })?;
        if card_id_changed {
            changed = true;
        }
        if apply_legacy_reversal_exclusions_to_deck(&mut data, &legacy_reversal_exclusions) {
            changed = true;
        }
        let prohibited_keys: Vec<_> = data
            .iter()
            .filter(|datum| datum.is_reversal_prohibited())
            .map(DatumKey::for_datum)
            .collect();
        if !prohibited_keys.is_empty() {
            store
                .set_datums_reversible(&prohibited_keys, false)
                .map_err(|source| {
                    localizer.could_not_open_review_database(review_db_path, &source.to_string())
                })?;
        }
        store.verify_deck_consistency(&data).map_err(|source| {
            localizer.could_not_open_review_database(review_db_path, &source.to_string())
        })?;
        if let Ok((now_unix_seconds, _)) = current_time_and_seed() {
            store
                .clear_inactive_card_states(now_unix_seconds)
                .map_err(|source| {
                    localizer.could_not_open_review_database(review_db_path, &source.to_string())
                })?;
        }
        if changed {
            let deck_text = format_deck_with_metadata(&deck_metadata, &data)
                .map_err(|error| localizer.could_not_save_deck(&error.to_string()))?;
            write_text_file_durably(deck_path, &deck_text)
                .map_err(|error| localizer.could_not_write_path(deck_path, &error.to_string()))?;
        }
        if !legacy_reversal_exclusions.is_empty() {
            store.clear_legacy_reversal_exclusions().map_err(|source| {
                localizer.could_not_open_review_database(review_db_path, &source.to_string())
            })?;
        }

        let selected_ready_key = data.first().map(DatumKey::for_datum);
        let ready_visible_indices = (0..data.len()).collect();
        let state = Self {
            localizer,
            settings,
            deck_path: deck_path.to_path_buf(),
            review_db_path: review_db_path.to_path_buf(),
            backup_dir: backup_dir.to_path_buf(),
            deck_metadata,
            selected_ready_key,
            selected_ready_index: if data.is_empty() { None } else { Some(0) },
            data,
            session: ReviewSession::Ready,
            ready_card_filter: ReadyCardFilter::All,
            ready_card_text_filter: String::new(),
            ready_card_sort: ReadyCardSort::DeckOrder,
            ready_card_sort_direction: ReadyCardSort::DeckOrder.default_direction(),
            ready_visible_indices: RefCell::new(ready_visible_indices),
            ready_list_sync_in_progress: Cell::new(false),
            last_ready_due_notification_count: None,
            store,
            last_status: String::new(),
            review_audio_player: RefCell::new(ReviewAudioPlayer::default()),
        };
        Ok(state)
    }

    #[cfg(test)]
    pub(crate) fn load_deck(deck_path: impl AsRef<Path>) -> Result<Self, String> {
        let deck_path = deck_path.as_ref();
        Self::load_with_backup_dir(
            deck_path,
            review_db_path_for_deck(deck_path),
            deck_path
                .parent()
                .unwrap_or_else(|| Path::new("."))
                .join("backups"),
            Localizer::default(),
            AppSettings::default(),
        )
    }

    pub(crate) fn load_deck_with_localizer(
        deck_path: impl AsRef<Path>,
        localizer: Localizer,
        settings: AppSettings,
    ) -> Result<Self, String> {
        let deck_path = deck_path.as_ref();
        Self::load_with_localizer(
            deck_path,
            review_db_path_for_deck(deck_path),
            localizer,
            settings,
        )
    }

    pub(crate) fn load_reversible_card_keys(&self) -> Result<HashSet<DatumKey>, String> {
        self.store
            .reversible_datum_keys()
            .map_err(|error| error.to_string())
    }

    pub(crate) fn load_reversal_excluded_card_keys(&self) -> Result<HashSet<DatumKey>, String> {
        Ok(self
            .data
            .iter()
            .filter(|datum| datum.is_reversal_prohibited())
            .map(DatumKey::for_datum)
            .collect())
    }

    pub(crate) fn study_items(&self) -> Result<Vec<StudyItem>, String> {
        Ok(study_items_for_deck(
            &self.data,
            &self.load_reversible_card_keys()?,
        ))
    }

    pub(crate) fn fallback_study_items(&self) -> Vec<StudyItem> {
        study_items_for_deck(&self.data, &HashSet::new())
    }

    pub(crate) fn current_datum(&self) -> Option<Datum> {
        self.current_study_item().map(|item| item.datum().clone())
    }

    fn current_deck_index(&self) -> Option<usize> {
        self.current_study_item().map(|item| item.deck_index())
    }

    pub(crate) fn current_study_item(&self) -> Option<StudyItem> {
        let index = self.session.current_deck_index()?;
        self.study_items()
            .unwrap_or_else(|_| self.fallback_study_items())
            .get(index)
            .cloned()
    }

    #[cfg(test)]
    pub(crate) fn selected_ready_datum(&self) -> Option<&Datum> {
        self.selected_ready_deck_index()
            .and_then(|index| self.data.get(index))
    }

    #[cfg(test)]
    fn selected_ready_deck_index(&self) -> Option<usize> {
        self.selected_ready_study_item()
            .ok()
            .flatten()
            .map(|item| item.deck_index())
    }

    pub(crate) fn selected_ready_study_item(&self) -> Result<Option<StudyItem>, String> {
        let Some(selected_key) = &self.selected_ready_key else {
            return Ok(None);
        };
        Ok(self
            .study_items()?
            .into_iter()
            .find(|item| item.datum_key() == selected_key))
    }

    pub(crate) fn selected_ready_card_details_text(&self) -> Result<Option<String>, String> {
        let Some(item) = self.selected_ready_study_item()? else {
            return Ok(None);
        };
        let datum = item.datum();
        let reviews = self
            .store
            .reviews_for_datum_key(item.datum_key())
            .map_err(|error| error.to_string())?;
        let schedule = self
            .store
            .schedule_for_datum_key(item.datum_key())
            .map_err(|error| error.to_string())?;
        let suspended = self.load_suspended_card_keys()?;
        let flagged = self.load_flagged_card_keys()?;
        let deferred = self.load_deferred_card_keys()?;
        let non_reversible = self.load_reversal_excluded_card_keys()?;
        let reversible = self.load_reversible_card_keys()?;
        let parent_key = item.parent_datum_key();
        let card_state = CardDetailsState {
            is_suspended: suspended.contains(item.datum_key()),
            is_flagged: flagged.contains(item.datum_key()),
            is_deferred: deferred.contains(item.datum_key()),
            is_non_reversible: non_reversible.contains(&parent_key),
            is_reversible: !item.is_reverse() && reversible.contains(&parent_key),
            is_reverse: item.is_reverse(),
        };
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);

        Ok(Some(card_details_text(
            self.localizer,
            item.datum_key(),
            datum,
            &reviews,
            schedule.as_ref(),
            card_state,
            now_unix_seconds,
        )))
    }

    pub(crate) fn learning_dashboard_text(&self) -> Result<String, String> {
        let items = self.study_items()?;
        let schedules = self.load_study_item_schedules(&items)?;
        let suspended = self.load_suspended_study_item_keys(&items)?;
        let mut reviews_by_key = HashMap::with_capacity(items.len());
        for item in &items {
            reviews_by_key.insert(
                item.datum_key().clone(),
                self.store
                    .reviews_for_datum_key(item.datum_key())
                    .map_err(|error| error.to_string())?,
            );
        }
        let dashboard_cards: Vec<_> = items
            .iter()
            .map(|item| LearningDashboardCard::new(item.datum_key().clone(), item.is_reverse()))
            .collect();
        let dashboard_card_keys = dashboard_cards
            .iter()
            .map(|card| card.datum_key().clone())
            .collect();
        let review_passes = self
            .store
            .review_pass_summaries_for_datum_keys(&dashboard_card_keys)
            .map_err(|error| error.to_string())?;
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        let report = learning_dashboard_report_with_calibration_rule(
            &dashboard_cards,
            &schedules,
            &suspended,
            &reviews_by_key,
            &review_passes,
            now_unix_seconds,
            self.settings.calibration_recall_rule,
        );

        Ok(learning_dashboard_text(self.localizer, &report))
    }

    pub(crate) fn deck_properties_text(&self) -> String {
        deck_properties_text(self.localizer, &self.deck_metadata, &self.data)
    }

    pub(crate) fn set_ready_card_filter(&mut self, filter: ReadyCardFilter) {
        self.ready_card_filter = filter;
        self.normalize_ready_selection_for_filter();
    }

    pub(crate) fn set_ready_card_text_filter(&mut self, text_filter: String) {
        self.ready_card_text_filter = text_filter;
        self.normalize_ready_selection_for_filter();
    }

    pub(crate) fn set_ready_card_sort(&mut self, sort: ReadyCardSort) {
        self.ready_card_sort = sort;
        self.ready_card_sort_direction = sort.default_direction();
        self.normalize_ready_selection_for_filter();
    }

    pub(crate) fn set_ready_card_sort_direction(&mut self, direction: ReadyCardSortDirection) {
        self.ready_card_sort_direction = direction;
        self.normalize_ready_selection_for_filter();
    }

    pub(crate) fn set_initial_ready_view_state(
        &mut self,
        filter: ReadyCardFilter,
        sort: ReadyCardSort,
        sort_direction: ReadyCardSortDirection,
    ) {
        self.ready_card_filter = filter;
        self.ready_card_sort = sort;
        self.ready_card_sort_direction = sort_direction;
        self.selected_ready_key = None;
        self.selected_ready_index = None;
        self.normalize_ready_selection_for_filter();
    }

    pub(crate) fn sync_ready_due_notification_baseline(&mut self) {
        self.last_ready_due_notification_count = match schedule_preview_if_started_now(self) {
            Ok(preview) => Some(preview.due_cards),
            Err(_) => None,
        };
    }

    pub(crate) fn normalize_ready_selection_for_filter(&mut self) {
        let visible_indices = self.ready_visible_indices_for_current_filter();
        let items = self
            .study_items()
            .unwrap_or_else(|_| self.fallback_study_items());
        let selected_index = self.selected_ready_key.as_ref().and_then(|selected_key| {
            items
                .iter()
                .position(|item| item.datum_key() == selected_key)
        });
        self.selected_ready_index = normalize_ready_selection(selected_index, &visible_indices);
        self.selected_ready_key = self
            .selected_ready_index
            .and_then(|index| items.get(index))
            .map(|item| item.datum_key().clone());
        *self.ready_visible_indices.borrow_mut() = visible_indices;
    }

    pub(crate) fn ready_visible_indices_for_current_filter(&self) -> Vec<usize> {
        self.try_ready_visible_indices_for_current_filter()
            .unwrap_or_else(|_| {
                let items = self.fallback_study_items();
                (0..items.len())
                    .filter(|index| {
                        ready_card_matches_text_filter(
                            items[*index].datum(),
                            &self.ready_card_text_filter,
                        )
                    })
                    .collect()
            })
    }

    pub(crate) fn try_ready_visible_indices_for_current_filter(
        &self,
    ) -> Result<Vec<usize>, String> {
        let items = self.study_items()?;
        let schedules = self.load_study_item_schedules(&items)?;
        let suspended = self.load_suspended_study_item_keys(&items)?;
        let flagged = self.load_flagged_study_item_keys(&items)?;
        let deferred = self.load_deferred_study_item_keys(&items)?;
        let non_reversible = self.load_reversal_excluded_card_keys()?;
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        let filter_state = ReadyStudyItemFilterState {
            schedules: &schedules,
            suspended: &suspended,
            flagged: &flagged,
            deferred: &deferred,
            non_reversible: &non_reversible,
            now_unix_seconds,
        };
        Ok(ready_study_item_indices_for_filter(
            &items,
            self.ready_card_filter,
            &self.ready_card_text_filter,
            self.ready_card_sort,
            self.ready_card_sort_direction,
            &filter_state,
        ))
    }

    fn ready_visible_study_item_keys_for_data(
        &self,
        data: &[Datum],
    ) -> Result<Vec<DatumKey>, String> {
        let reversible = self.load_reversible_card_keys()?;
        let items = study_items_for_deck(data, &reversible);
        let schedules = self.load_study_item_schedules(&items)?;
        let suspended = self.load_suspended_study_item_keys(&items)?;
        let flagged = self.load_flagged_study_item_keys(&items)?;
        let deferred = self.load_deferred_study_item_keys(&items)?;
        let non_reversible = self.load_reversal_excluded_card_keys()?;
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        let filter_state = ReadyStudyItemFilterState {
            schedules: &schedules,
            suspended: &suspended,
            flagged: &flagged,
            deferred: &deferred,
            non_reversible: &non_reversible,
            now_unix_seconds,
        };
        Ok(ready_study_item_indices_for_filter(
            &items,
            self.ready_card_filter,
            &self.ready_card_text_filter,
            self.ready_card_sort,
            self.ready_card_sort_direction,
            &filter_state,
        )
        .into_iter()
        .filter_map(|index| items.get(index))
        .map(|item| item.datum_key().clone())
        .collect())
    }

    fn visible_selection_successor_candidates(
        &self,
        selected_key: &DatumKey,
    ) -> Result<Vec<DatumKey>, String> {
        let visible_keys = self.ready_visible_study_item_keys_for_data(&self.data)?;
        let Some(selected_position) = visible_keys.iter().position(|key| key == selected_key)
        else {
            return Ok(visible_keys);
        };

        Ok(visible_keys[selected_position + 1..]
            .iter()
            .chain(visible_keys[..selected_position].iter().rev())
            .cloned()
            .collect())
    }

    fn selected_visible_key_after_change(
        &self,
        data: &[Datum],
        candidates: &[DatumKey],
    ) -> Result<Option<DatumKey>, String> {
        let visible_keys = self.ready_visible_study_item_keys_for_data(data)?;
        Ok(candidates
            .iter()
            .find(|candidate| visible_keys.iter().any(|key| key == *candidate))
            .cloned()
            .or_else(|| visible_keys.first().cloned()))
    }

    pub(crate) fn select_ready_index(&mut self, index: usize) {
        let items = self
            .study_items()
            .unwrap_or_else(|_| self.fallback_study_items());
        if let Some(item) = items.get(index) {
            self.selected_ready_index = Some(index);
            self.selected_ready_key = Some(item.datum_key().clone());
        }
    }

    pub(crate) fn start_cycle(&mut self) {
        match current_time_and_seed() {
            Ok((now_unix_seconds, filler_seed)) => {
                self.start_cycle_with_config(SchedulerConfig::new(
                    self.settings.minimum_cards_per_session,
                    now_unix_seconds,
                    filler_seed,
                ));
            }
            Err(error) => {
                self.last_status = self.localizer.could_not_start_scheduled_pass(&error);
            }
        }
    }

    pub(crate) fn start_cycle_with_config(&mut self, config: SchedulerConfig) {
        if self.data.is_empty() {
            self.last_status = self
                .localizer
                .no_cards_available_to_start_pass()
                .to_string();
            return;
        }

        if let Err(error) = self.clear_inactive_card_states(config.now_unix_seconds()) {
            self.session.reset();
            self.last_status = self.localizer.could_not_build_review_queue(&error);
            return;
        }

        let review_queue = match self.build_review_queue(config) {
            Ok(review_queue) => review_queue,
            Err(error) => {
                self.session.reset();
                self.last_status = error;
                return;
            }
        };

        if review_queue.is_empty() {
            self.session.reset();
            self.last_status = self
                .localizer
                .no_cards_scheduled_to_start_pass(self.settings.minimum_cards_per_session);
            return;
        }

        let review_queue_len = review_queue.len();
        let study_item_count = self
            .study_items()
            .unwrap_or_else(|_| self.fallback_study_items())
            .len();
        self.session = ReviewSession::start(
            review_queue,
            study_item_count,
            true,
            config.filler_seed(),
            config.now_unix_seconds(),
        )
        .expect("non-empty review queue checked before session start");
        self.last_ready_due_notification_count = None;
        let backup_warning = self.backup_review_database_on_session_start();
        self.last_status = append_status_warning(
            self.localizer.started_pass(review_queue_len),
            backup_warning,
        );
    }

    pub(crate) fn force_start_cycle(&mut self) {
        match current_time_and_seed() {
            Ok((now_unix_seconds, filler_seed)) => {
                self.force_start_cycle_with_seed(filler_seed, now_unix_seconds)
            }
            Err(error) => {
                self.last_status = self.localizer.could_not_start_scheduled_pass(&error);
            }
        }
    }

    pub(crate) fn force_start_cycle_with_seed(&mut self, seed: u64, now_unix_seconds: i64) {
        if self.data.is_empty() {
            self.last_status = self
                .localizer
                .no_cards_available_to_start_pass()
                .to_string();
            return;
        }

        if let Err(error) = self.clear_inactive_card_states(now_unix_seconds) {
            self.session.reset();
            self.last_status = self.localizer.could_not_build_review_queue(&error);
            return;
        }

        let items = match self.study_items() {
            Ok(items) => items,
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
                return;
            }
        };
        let suspended = match self.load_suspended_study_item_keys(&items) {
            Ok(suspended) => suspended,
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
                return;
            }
        };
        let deferred = match self.load_deferred_study_item_keys_at(&items, now_unix_seconds) {
            Ok(deferred) => deferred,
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
                return;
            }
        };
        let mut review_queue: Vec<usize> = items
            .iter()
            .enumerate()
            .filter_map(|(index, item)| {
                (!suspended.contains(item.datum_key()) && !deferred.contains(item.datum_key()))
                    .then_some(index)
            })
            .collect();
        if review_queue.is_empty() {
            self.last_status = self
                .localizer
                .no_cards_available_to_start_pass()
                .to_string();
            return;
        }
        shuffle_indices(&mut review_queue, seed);
        let review_queue_len = review_queue.len();
        self.session =
            ReviewSession::start(review_queue, items.len(), false, seed, now_unix_seconds)
                .expect("non-empty deck creates a non-empty forced review queue");
        self.last_ready_due_notification_count = None;
        let backup_warning = self.backup_review_database_on_session_start();
        self.last_status = append_status_warning(
            self.localizer.force_started_pass(review_queue_len),
            backup_warning,
        );
    }

    pub(crate) fn review_flagged_cards(&mut self) {
        match current_time_and_seed() {
            Ok((now_unix_seconds, seed)) => {
                self.review_flagged_cards_with_seed(seed, now_unix_seconds)
            }
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
            }
        }
    }

    pub(crate) fn review_flagged_cards_with_seed(&mut self, seed: u64, now_unix_seconds: i64) {
        if self.data.is_empty() {
            self.last_status = self
                .localizer
                .no_flagged_cards_available_to_review()
                .to_string();
            return;
        }

        if let Err(error) = self.clear_inactive_card_states(now_unix_seconds) {
            self.session.reset();
            self.last_status = self.localizer.could_not_build_review_queue(&error);
            return;
        }

        let items = match self.study_items() {
            Ok(items) => items,
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
                return;
            }
        };
        let mut review_queue = match self.available_flagged_review_queue(&items, now_unix_seconds) {
            Ok(review_queue) => review_queue,
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
                return;
            }
        };
        if review_queue.is_empty() {
            self.last_status = self
                .localizer
                .no_flagged_cards_available_to_review()
                .to_string();
            return;
        }

        shuffle_indices(&mut review_queue, seed);
        let review_queue_len = review_queue.len();
        self.session =
            ReviewSession::start(review_queue, items.len(), false, seed, now_unix_seconds)
                .expect("non-empty flagged review queue checked before session start");
        self.last_ready_due_notification_count = None;
        let backup_warning = self.backup_review_database_on_session_start();
        self.last_status = append_status_warning(
            self.localizer.started_flagged_pass(review_queue_len),
            backup_warning,
        );
    }

    pub(crate) fn start_exam_pass(&mut self, request: ExamSizeRequest) -> Option<ExamPass> {
        match current_time_and_seed() {
            Ok((now_unix_seconds, filler_seed)) => {
                self.start_exam_pass_with_request(request, filler_seed, now_unix_seconds)
            }
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
                None
            }
        }
    }

    pub(crate) fn start_exam_pass_with_request(
        &mut self,
        request: ExamSizeRequest,
        seed: u64,
        now_unix_seconds: i64,
    ) -> Option<ExamPass> {
        if self.data.is_empty() {
            self.last_status = self
                .localizer
                .no_cards_available_to_start_pass()
                .to_string();
            return None;
        }

        if let Err(error) = self.clear_inactive_card_states(now_unix_seconds) {
            self.session.reset();
            self.last_status = self.localizer.could_not_build_review_queue(&error);
            return None;
        }

        let items = match self.study_items() {
            Ok(items) => items,
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
                return None;
            }
        };
        let mut exam_queue = match self.active_study_item_indices_at(&items, now_unix_seconds) {
            Ok(active_indices) => active_indices,
            Err(error) => {
                self.last_status = self.localizer.could_not_build_review_queue(&error);
                return None;
            }
        };

        if exam_queue.is_empty() {
            self.session.reset();
            self.last_status = self
                .localizer
                .no_cards_available_to_start_pass()
                .to_string();
            return None;
        }

        shuffle_indices(&mut exam_queue, seed);
        let question_count = resolve_exam_question_count(request, exam_queue.len());
        exam_queue.truncate(question_count);
        let session_queue = exam_queue.clone();
        let questions: Vec<_> = exam_queue
            .into_iter()
            .filter_map(|index| items.get(index))
            .map(|item| {
                ExamQuestion::new(
                    item.datum_key().clone(),
                    item.datum().prompt(),
                    item.datum().response(),
                )
            })
            .collect();
        if questions.is_empty() {
            self.session.reset();
            self.last_status = self
                .localizer
                .no_cards_available_to_start_pass()
                .to_string();
            return None;
        }

        let question_count = questions.len();
        self.session =
            ReviewSession::start(session_queue, items.len(), false, seed, now_unix_seconds)
                .expect("non-empty exam queue checked before session start");
        self.last_ready_due_notification_count = None;
        self.last_status = self.localizer.started_exam_pass(question_count);

        Some(ExamPass::new(now_unix_seconds, questions))
    }

    pub(crate) fn finish_exam_pass(
        &mut self,
        exam_pass: &ExamPass,
        answers: &[GradedExamAnswer],
        finished_at_unix_seconds: i64,
    ) -> ExamSummary {
        let finished_at_unix_seconds =
            finished_at_unix_seconds.max(exam_pass.started_at_unix_seconds());
        let elapsed_seconds =
            finished_at_unix_seconds.saturating_sub(exam_pass.started_at_unix_seconds()) as u64;
        self.session.reset();
        self.normalize_ready_selection_for_filter();

        if answers.is_empty() {
            self.last_status = self.localizer.no_cards_reviewed_message().to_string();
        } else {
            self.last_status = self.localizer.completed_exam_pass(answers.len());
        }

        ExamSummary::from_answers(exam_pass.questions().len(), answers, elapsed_seconds)
    }

    fn backup_review_database_on_session_start(&self) -> Option<String> {
        if self.settings.review_db_backup_policy == ReviewDbBackupPolicy::OnSessionStart {
            return backup_review_database(&self.review_db_path, &self.backup_dir, &self.store)
                .err()
                .map(|error| {
                    self.localizer.could_not_backup_review_database_status(
                        &self.review_db_path.display().to_string(),
                        &self.backup_dir.display().to_string(),
                        &error.to_string(),
                    )
                });
        }
        None
    }

    fn backup_review_database_before_significant_change(&self) -> Result<(), String> {
        if self.settings.review_db_backup_policy == ReviewDbBackupPolicy::Off {
            return Ok(());
        }

        backup_review_database(&self.review_db_path, &self.backup_dir, &self.store)
            .map(|_| ())
            .map_err(|error| {
                self.localizer.could_not_backup_review_database(
                    &self.review_db_path.display().to_string(),
                    &self.backup_dir.display().to_string(),
                    &error.to_string(),
                )
            })
    }

    pub(crate) fn show_response(&mut self, guess: PromptGuess) {
        self.session.show_response(guess);
        self.last_status = self.localizer.response_shown_choose_feedback().to_string();
    }

    pub(crate) fn record_feedback_and_check_for_cycle_completion(
        &mut self,
        feedback: RecallFeedback,
    ) -> Option<ReviewCycleSummary> {
        let Some(item) = self.current_study_item() else {
            self.last_status = self.localizer.no_datum_available_to_record().to_string();
            return None;
        };

        let Some(guess) = self.session.response_guess() else {
            self.last_status = self
                .localizer
                .choose_prompt_guess_before_feedback()
                .to_string();
            return None;
        };

        let review = Review::new(guess, feedback);
        match self
            .store
            .record_review_for_datum_key_now(item.datum_key(), review.clone())
        {
            Ok(event) => {
                let progress = self
                    .session
                    .record_completed_review(
                        review,
                        feedback,
                        event.id(),
                        event.reviewed_at_unix_seconds(),
                    )
                    .expect("recording feedback requires a running review session");
                if let Some(summary) = progress.completed_summary {
                    let pass_warning = self.record_review_pass_summary(&summary).err();
                    self.last_status = self
                        .localizer
                        .recorded_review_completed(event.id(), progress.total_cards);
                    self.last_status =
                        append_status_warning(self.last_status.clone(), pass_warning);
                    Some(summary)
                } else {
                    self.last_status = self.localizer.recorded_review_for_card(
                        event.id(),
                        progress.completed_index,
                        progress.total_cards,
                    );
                    None
                }
            }
            Err(error) => {
                self.last_status = self.localizer.could_not_record_review(
                    &self.review_db_path.display().to_string(),
                    &error.to_string(),
                );
                None
            }
        }
    }

    pub(crate) fn end_current_pass(&mut self) -> Option<ReviewCycleSummary> {
        let ended_at_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        let Some(summary) = self.session.end_current_pass(ended_at_unix_seconds) else {
            self.last_status = self.localizer.no_cards_available_for_review().to_string();
            return None;
        };
        let pass_warning = self.record_review_pass_summary(&summary).err();
        self.last_status = self
            .localizer
            .ended_pass_early_after(summary.stats.reviewed_cards, summary.total_cards);
        self.last_status = append_status_warning(self.last_status.clone(), pass_warning);
        Some(summary)
    }

    fn record_review_pass_summary(&self, summary: &ReviewCycleSummary) -> Result<(), String> {
        let pass = ReviewPass::new(
            StoredReviewPassSummary::new(
                summary.started_at_unix_seconds,
                summary.finished_at_unix_seconds,
                summary.was_scheduled,
                summary.ended_early,
                summary.initial_cards,
                summary.total_cards,
                summary.stats.unique_reviewed_cards,
            ),
            summary.items.clone(),
        );
        self.store
            .record_review_pass(&pass)
            .map(|_| ())
            .map_err(|error| {
                self.localizer.could_not_record_review_pass_status(
                    &self.review_db_path.display().to_string(),
                    &error.to_string(),
                )
            })
    }

    pub(crate) fn restart_cycle(&mut self) {
        self.session.reset();
        self.normalize_ready_selection_for_filter();
        self.last_ready_due_notification_count = None;
        self.last_status = self.localizer.ready_to_start_new_pass(self.data.len());
    }

    pub(crate) fn add_card(&mut self, datum: Datum) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        let mut updated_data = self.data.clone();
        let card_id = self.next_available_card_id()?;
        let selected_key = DatumKey::for_card_id(card_id);
        updated_data.push(datum.with_card_id(card_id));
        self.ensure_reversal_invariants(&updated_data)?;
        let total_cards = updated_data.len();
        self.replace_deck(
            updated_data,
            Some(selected_key),
            self.localizer.added_card_status(total_cards),
        )
    }

    pub(crate) fn edit_selected_card(&mut self, datum: Datum) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        let Some(item) = self.selected_ready_study_item()? else {
            return Err(self.localizer.no_selected_card_to_edit().to_string());
        };
        if item.is_reverse() {
            return Err(self
                .localizer
                .generated_reverse_card_cannot_be_edited()
                .to_string());
        }
        let selected_index = item.deck_index();

        let mut updated_data = self.data.clone();
        let existing_card_id = updated_data[selected_index].card_id();
        let existing_reversal_policy = updated_data[selected_index].reversal_policy();
        updated_data[selected_index] = if let Some(card_id) = existing_card_id {
            datum
                .with_card_id(card_id)
                .with_reversal_policy(existing_reversal_policy)
        } else {
            datum.with_reversal_policy(existing_reversal_policy)
        };
        self.ensure_reversal_invariants(&updated_data)?;
        let total_cards = updated_data.len();
        let selected_key = DatumKey::for_datum(&updated_data[selected_index]);
        self.replace_deck(
            updated_data,
            Some(selected_key),
            self.localizer.saved_selected_card_status(total_cards),
        )
    }

    pub(crate) fn edit_current_revealed_card(&mut self, datum: Datum) -> Result<(), String> {
        if !matches!(self.session.phase(), ReviewPhase::Response { .. }) {
            return Err(self
                .localizer
                .choose_prompt_guess_before_feedback()
                .to_string());
        }
        let Some(deck_index) = self.current_deck_index() else {
            return Err(self.localizer.no_datum_available_to_record().to_string());
        };
        if self
            .current_study_item()
            .is_some_and(|item| item.is_reverse())
        {
            return Err(self
                .localizer
                .generated_reverse_card_cannot_be_edited()
                .to_string());
        }

        let mut updated_data = self.data.clone();
        let existing_card_id = updated_data[deck_index].card_id();
        let existing_reversal_policy = updated_data[deck_index].reversal_policy();
        updated_data[deck_index] = if let Some(card_id) = existing_card_id {
            datum
                .with_card_id(card_id)
                .with_reversal_policy(existing_reversal_policy)
        } else {
            datum.with_reversal_policy(existing_reversal_policy)
        };
        self.ensure_reversal_invariants(&updated_data)?;
        let deck_text = format_deck_with_metadata(&self.deck_metadata, &updated_data)
            .map_err(|error| self.localizer.could_not_save_deck(&error.to_string()))?;
        write_deck_file_durably(
            &self.deck_path,
            &self.backup_dir,
            self.settings.deck_backup_policy,
            &deck_text,
        )
        .map_err(|error| {
            self.localizer
                .could_not_write_path(&self.deck_path, &error.to_string())
        })?;

        self.data = updated_data;
        self.last_status = self.localizer.saved_revealed_card_status().to_string();
        Ok(())
    }

    pub(crate) fn update_deck_metadata(
        &mut self,
        mut deck_metadata: DeckMetadata,
    ) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        deck_metadata.ensure_v2_header();
        let deck_text = format_deck_with_metadata(&deck_metadata, &self.data)
            .map_err(|error| self.localizer.could_not_save_deck(&error.to_string()))?;
        write_deck_file_durably(
            &self.deck_path,
            &self.backup_dir,
            self.settings.deck_backup_policy,
            &deck_text,
        )
        .map_err(|error| {
            self.localizer
                .could_not_write_path(&self.deck_path, &error.to_string())
        })?;

        self.deck_metadata = deck_metadata;
        self.last_status = self.localizer.saved_deck_properties_status().to_string();
        Ok(())
    }

    pub(crate) fn remove_selected_card(&mut self) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        let Some(item) = self.selected_ready_study_item()? else {
            return Err(self.localizer.no_selected_card_to_remove().to_string());
        };
        let selected_key = item.datum_key().clone();
        let selection_candidates = self.visible_selection_successor_candidates(&selected_key)?;
        if item.is_reverse() {
            let parent_key = item.parent_datum_key();
            let mut reverse_selection_candidates =
                Vec::with_capacity(selection_candidates.len() + 1);
            reverse_selection_candidates.push(parent_key.clone());
            reverse_selection_candidates.extend(
                selection_candidates
                    .into_iter()
                    .filter(|candidate| candidate != &parent_key),
            );
            self.store
                .set_datum_reversible(&parent_key, false)
                .map_err(|error| error.to_string())?;
            self.last_status = self.localizer.removed_reverse_card_status().to_string();
            self.selected_ready_key =
                self.selected_visible_key_after_change(&self.data, &reverse_selection_candidates)?;
            self.normalize_ready_selection_for_filter();
            return Ok(());
        }
        let selected_index = item.deck_index();

        let removed_card_number = selected_index + 1;
        let mut updated_data = self.data.clone();
        updated_data.remove(selected_index);
        let next_selected_key =
            self.selected_visible_key_after_change(&updated_data, &selection_candidates)?;

        if updated_data.is_empty() {
            self.replace_deck(
                updated_data,
                None,
                self.localizer.removed_last_card_status().to_string(),
            )
        } else {
            let total_cards = updated_data.len();
            self.replace_deck(
                updated_data,
                next_selected_key,
                self.localizer
                    .removed_card_status(removed_card_number, total_cards),
            )
        }
    }

    pub(crate) fn toggle_selected_card_suspension(&mut self) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        let Some(item) = self.selected_ready_study_item()? else {
            return Err(self.localizer.no_selected_card_to_edit().to_string());
        };
        let datum_key = item.datum_key().clone();
        let suspended = self.load_suspended_card_keys()?;
        let should_suspend = !suspended.contains(&datum_key);
        self.store
            .set_datum_suspended(&datum_key, should_suspend)
            .map_err(|error| error.to_string())?;
        self.last_status = if should_suspend {
            self.localizer.suspended_card_status().to_string()
        } else {
            self.localizer.unsuspended_card_status().to_string()
        };
        self.normalize_ready_selection_for_filter();
        Ok(())
    }

    pub(crate) fn toggle_current_or_selected_card_flag(&mut self) -> Result<(), String> {
        let item = if self.session.is_ready() {
            self.selected_ready_study_item()?
        } else {
            self.current_study_item()
        };
        let Some(item) = item else {
            return Err(self.localizer.no_selected_card_to_edit().to_string());
        };
        let datum_key = item.datum_key().clone();
        let flagged = self.load_flagged_card_keys()?;
        let should_flag = !flagged.contains(&datum_key);
        self.store
            .set_datum_flagged(&datum_key, should_flag)
            .map_err(|error| error.to_string())?;
        self.last_status = if should_flag {
            self.localizer.flagged_card_status().to_string()
        } else {
            self.localizer.unflagged_card_status().to_string()
        };
        if self.session.is_ready() {
            self.normalize_ready_selection_for_filter();
        }
        Ok(())
    }

    pub(crate) fn toggle_selected_card_deferral(&mut self) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        let Some(item) = self.selected_ready_study_item()? else {
            return Err(self.localizer.no_selected_card_to_edit().to_string());
        };
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .map_err(|error| error.to_string())?;
        let datum_key = item.datum_key().clone();
        let deferred = self.load_deferred_card_keys_at(now_unix_seconds)?;
        let should_defer = !deferred.contains(&datum_key);
        if should_defer {
            self.store
                .defer_datum_until(&datum_key, now_unix_seconds + DEFAULT_DEFER_SECONDS)
                .map_err(|error| error.to_string())?;
            self.last_status = self.localizer.deferred_card_status().to_string();
        } else {
            self.store
                .clear_datum_deferred(&datum_key)
                .map_err(|error| error.to_string())?;
            self.last_status = self.localizer.cleared_card_deferral_status().to_string();
        }
        self.normalize_ready_selection_for_filter();
        Ok(())
    }

    pub(crate) fn defer_current_review_card(&mut self) -> Result<ReviewDeferralProgress, String> {
        if !matches!(self.session.phase(), ReviewPhase::Prompt) {
            return Err(self
                .localizer
                .choose_prompt_guess_before_feedback()
                .to_string());
        }
        let Some(item) = self.current_study_item() else {
            return Err(self.localizer.no_datum_available_to_record().to_string());
        };
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .map_err(|error| error.to_string())?;
        let datum_key = item.datum_key().clone();
        self.store
            .defer_datum_until(&datum_key, now_unix_seconds + DEFAULT_DEFER_SECONDS)
            .map_err(|error| error.to_string())?;
        let Some(progress) = self.session.defer_current_prompt(now_unix_seconds) else {
            return Err(self
                .localizer
                .choose_prompt_guess_before_feedback()
                .to_string());
        };

        let pass_warning = progress
            .completed_summary()
            .and_then(|summary| self.record_review_pass_summary(summary).err());
        self.last_status = append_status_warning(
            self.localizer.deferred_card_status().to_string(),
            pass_warning,
        );
        if progress.pass_finished() {
            self.normalize_ready_selection_for_filter();
        }
        Ok(progress)
    }

    pub(crate) fn toggle_selected_card_reversal(&mut self) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        let Some(item) = self.selected_ready_study_item()? else {
            return Err(self.localizer.no_selected_card_to_edit().to_string());
        };

        if item.is_reverse() {
            let parent_key = item.parent_datum_key();
            self.store
                .set_datum_reversible(&parent_key, false)
                .map_err(|error| error.to_string())?;
            self.last_status = self.localizer.removed_reverse_card_status().to_string();
            self.selected_ready_key = Some(parent_key);
            self.normalize_ready_selection_for_filter();
            return Ok(());
        }

        let parent_key = item.parent_datum_key();
        let reversible = self.load_reversible_card_keys()?;
        if reversible.contains(&parent_key) {
            self.store
                .set_datum_reversible(&parent_key, false)
                .map_err(|error| error.to_string())?;
            self.last_status = self.localizer.removed_reverse_card_status().to_string();
        } else {
            let excluded = self.load_reversal_excluded_card_keys()?;
            if excluded.contains(&parent_key) {
                return Err(self.localizer.cannot_reverse_excluded_card().to_string());
            }
            if !response_is_unambiguous_for_reversal(&self.data, item.deck_index()) {
                return Err(self
                    .localizer
                    .cannot_reverse_ambiguous_response()
                    .to_string());
            }
            self.store
                .set_datum_reversible(&parent_key, true)
                .map_err(|error| error.to_string())?;
            self.last_status = self.localizer.marked_card_reversible_status().to_string();
        }
        self.normalize_ready_selection_for_filter();
        Ok(())
    }

    pub(crate) fn toggle_selected_card_reversal_exclusion(&mut self) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        let Some(item) = self.selected_ready_study_item()? else {
            return Err(self.localizer.no_selected_card_to_edit().to_string());
        };
        let parent_key = item.parent_datum_key();
        let excluded = self.load_reversal_excluded_card_keys()?;
        let should_exclude = !excluded.contains(&parent_key);
        if should_exclude {
            self.store
                .set_datum_reversible(&parent_key, false)
                .map_err(|error| error.to_string())?;
        }
        let status_message = if should_exclude {
            self.localizer
                .marked_card_non_reversible_status()
                .to_string()
        } else {
            self.localizer.allowed_card_reversal_status().to_string()
        };
        let mut updated_data = self.data.clone();
        let Some(datum) = updated_data.get_mut(item.deck_index()) else {
            return Err(self.localizer.no_selected_card_to_edit().to_string());
        };
        *datum = datum.clone().with_reversal_prohibited(should_exclude);
        let selected_key = if item.is_reverse() {
            Some(parent_key)
        } else {
            Some(item.datum_key().clone())
        };
        self.replace_deck(updated_data, selected_key, status_message)
    }

    pub(crate) fn eligible_reversible_card_count(&self) -> Result<usize, String> {
        let reversible = self.load_reversible_card_keys()?;
        let excluded = self.load_reversal_excluded_card_keys()?;
        Ok(reversible_candidate_indices(&self.data, &reversible, &excluded).len())
    }

    pub(crate) fn make_eligible_cards_reversible(&mut self) -> Result<usize, String> {
        self.ensure_card_modification_is_allowed()?;
        let reversible = self.load_reversible_card_keys()?;
        let excluded = self.load_reversal_excluded_card_keys()?;
        let candidate_indices = reversible_candidate_indices(&self.data, &reversible, &excluded);
        if !candidate_indices.is_empty() {
            self.backup_review_database_before_significant_change()?;
        }
        let candidate_keys = candidate_indices
            .iter()
            .map(|index| DatumKey::for_datum(&self.data[*index]))
            .collect::<Vec<_>>();
        self.store
            .set_datums_reversible_in_bulk(&candidate_keys)
            .map_err(|error| error.to_string())?;
        let updated_count = candidate_indices.len();
        self.last_status = self.localizer.marked_cards_reversible_status(updated_count);
        self.normalize_ready_selection_for_filter();
        Ok(updated_count)
    }

    pub(crate) fn latest_bulk_reversal_count(&self) -> Result<usize, String> {
        self.store
            .latest_bulk_reversal_count()
            .map_err(|error| error.to_string())
    }

    pub(crate) fn undo_latest_bulk_reversal(&mut self) -> Result<usize, String> {
        self.ensure_card_modification_is_allowed()?;
        let card_count = self.latest_bulk_reversal_count()?;
        if card_count == 0 {
            return Err(self.localizer.no_bulk_reversal_to_undo().to_string());
        }
        self.backup_review_database_before_significant_change()?;
        let removed_count = self
            .store
            .remove_latest_bulk_reversal()
            .map_err(|error| error.to_string())?;
        self.last_status = self
            .localizer
            .removed_latest_bulk_reversal_status(removed_count);
        self.normalize_ready_selection_for_filter();
        Ok(removed_count)
    }

    fn active_review_database_keys(
        &self,
    ) -> Result<(HashSet<DatumKey>, HashSet<DatumKey>), String> {
        let parent_keys: HashSet<DatumKey> = self.data.iter().map(DatumKey::for_datum).collect();
        let reversible_keys = self.load_reversible_card_keys()?;
        let mut active_keys = parent_keys.clone();
        for datum_key in reversible_keys {
            if parent_keys.contains(&datum_key) {
                active_keys.insert(DatumKey::for_reverse_of(&datum_key));
            }
        }
        Ok((active_keys, parent_keys))
    }

    pub(crate) fn clear_inactive_card_states(
        &self,
        now_unix_seconds: i64,
    ) -> Result<usize, String> {
        self.store
            .clear_inactive_card_states(now_unix_seconds)
            .map_err(|error| error.to_string())
    }

    pub(crate) fn scrub_review_database(&mut self) -> Result<usize, String> {
        self.ensure_card_modification_is_allowed()?;
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .map_err(|error| error.to_string())?;
        self.backup_review_database_before_significant_change()?;
        let cleared_state_rows = self.clear_inactive_card_states(now_unix_seconds)?;
        let (active_keys, parent_keys) = self.active_review_database_keys()?;
        let summary = self
            .store
            .scrub_inactive_data(&active_keys, &parent_keys)
            .map_err(|error| error.to_string())?;
        self.store
            .verify_deck_consistency(&self.data)
            .map_err(|error| error.to_string())?;
        let removed_rows = summary.total_removed_rows() + cleared_state_rows;
        self.last_status = self.localizer.scrubbed_review_database_status(removed_rows);
        Ok(removed_rows)
    }

    pub(crate) fn reclaim_database_space(&mut self) -> Result<(), String> {
        self.ensure_card_modification_is_allowed()?;
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .map_err(|error| error.to_string())?;
        self.clear_inactive_card_states(now_unix_seconds)?;
        self.store
            .reclaim_space()
            .map_err(|error| error.to_string())?;
        self.last_status = self.localizer.reclaimed_database_space_status().to_string();
        Ok(())
    }

    fn replace_deck(
        &mut self,
        updated_data: Vec<Datum>,
        selected_ready_key: Option<DatumKey>,
        status_message: String,
    ) -> Result<(), String> {
        let deck_text = format_deck_with_metadata(&self.deck_metadata, &updated_data)
            .map_err(|error| self.localizer.could_not_save_deck(&error.to_string()))?;
        write_deck_file_durably(
            &self.deck_path,
            &self.backup_dir,
            self.settings.deck_backup_policy,
            &deck_text,
        )
        .map_err(|error| {
            self.localizer
                .could_not_write_path(&self.deck_path, &error.to_string())
        })?;

        self.data = updated_data;
        self.session.reset();
        self.selected_ready_key = selected_ready_key;
        self.normalize_ready_selection_for_filter();
        self.last_status = status_message;
        Ok(())
    }

    fn ensure_card_modification_is_allowed(&self) -> Result<(), String> {
        if self.session.is_ready() {
            Ok(())
        } else {
            Err(self
                .localizer
                .card_modification_only_before_cycle_starts()
                .to_string())
        }
    }

    fn ensure_reversal_invariants(&self, data: &[Datum]) -> Result<(), String> {
        let reversible = self.load_reversible_card_keys()?;
        for (index, datum) in data.iter().enumerate() {
            if reversible.contains(&DatumKey::for_datum(datum))
                && !response_is_unambiguous_for_reversal(data, index)
            {
                return Err(self
                    .localizer
                    .cannot_reverse_ambiguous_response()
                    .to_string());
            }
        }
        Ok(())
    }

    fn build_review_queue(&self, config: SchedulerConfig) -> Result<Vec<usize>, String> {
        let items = self
            .study_items()
            .map_err(|error| self.localizer.could_not_build_review_queue(&error))?;
        let schedules = self
            .load_study_item_schedules(&items)
            .map_err(|error| self.localizer.could_not_build_review_queue(&error))?;
        let suspended = self
            .load_suspended_study_item_keys(&items)
            .map_err(|error| self.localizer.could_not_build_review_queue(&error))?;
        let deferred = self
            .load_deferred_study_item_keys_at(&items, config.now_unix_seconds())
            .map_err(|error| self.localizer.could_not_build_review_queue(&error))?;
        let active_indices: Vec<usize> = items
            .iter()
            .enumerate()
            .filter_map(|(index, item)| {
                (!suspended.contains(item.datum_key()) && !deferred.contains(item.datum_key()))
                    .then_some(index)
            })
            .collect();
        let active_data: Vec<(DatumKey, &Datum)> = active_indices
            .iter()
            .map(|index| (items[*index].datum_key().clone(), items[*index].datum()))
            .collect();

        Ok(schedule_session_for_keys(&active_data, &schedules, config)
            .into_iter()
            .map(|card| active_indices[card.deck_index()])
            .collect())
    }

    fn active_study_item_indices_at(
        &self,
        items: &[StudyItem],
        now_unix_seconds: i64,
    ) -> Result<Vec<usize>, String> {
        let suspended = self.load_suspended_study_item_keys(items)?;
        let deferred = self.load_deferred_study_item_keys_at(items, now_unix_seconds)?;
        Ok(items
            .iter()
            .enumerate()
            .filter_map(|(index, item)| {
                (!suspended.contains(item.datum_key()) && !deferred.contains(item.datum_key()))
                    .then_some(index)
            })
            .collect())
    }

    pub(crate) fn load_study_item_schedules(
        &self,
        items: &[StudyItem],
    ) -> Result<HashMap<DatumKey, CardSchedule>, String> {
        let mut schedules = HashMap::with_capacity(items.len());
        for item in items {
            let Some(schedule) = self
                .store
                .schedule_for_datum_key(item.datum_key())
                .map_err(|error| error.to_string())?
            else {
                continue;
            };
            schedules.insert(item.datum_key().clone(), schedule);
        }

        Ok(schedules)
    }

    pub(crate) fn load_suspended_card_keys(&self) -> Result<HashSet<DatumKey>, String> {
        self.store
            .suspended_datum_keys()
            .map_err(|error| error.to_string())
    }

    pub(crate) fn load_suspended_study_item_keys(
        &self,
        items: &[StudyItem],
    ) -> Result<HashSet<DatumKey>, String> {
        let suspended = self.load_suspended_card_keys()?;
        Ok(items
            .iter()
            .filter_map(|item| {
                suspended
                    .contains(item.datum_key())
                    .then_some(item.datum_key().clone())
            })
            .collect())
    }

    pub(crate) fn load_flagged_card_keys(&self) -> Result<HashSet<DatumKey>, String> {
        self.store
            .flagged_datum_keys()
            .map_err(|error| error.to_string())
    }

    pub(crate) fn load_flagged_study_item_keys(
        &self,
        items: &[StudyItem],
    ) -> Result<HashSet<DatumKey>, String> {
        let flagged = self.load_flagged_card_keys()?;
        Ok(items
            .iter()
            .filter_map(|item| {
                flagged
                    .contains(item.datum_key())
                    .then_some(item.datum_key().clone())
            })
            .collect())
    }

    fn load_deferred_card_keys_at(
        &self,
        now_unix_seconds: i64,
    ) -> Result<HashSet<DatumKey>, String> {
        self.store
            .deferred_datum_keys(now_unix_seconds)
            .map_err(|error| error.to_string())
    }

    pub(crate) fn load_deferred_card_keys(&self) -> Result<HashSet<DatumKey>, String> {
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        self.load_deferred_card_keys_at(now_unix_seconds)
    }

    pub(crate) fn load_deferred_study_item_keys_at(
        &self,
        items: &[StudyItem],
        now_unix_seconds: i64,
    ) -> Result<HashSet<DatumKey>, String> {
        let deferred = self.load_deferred_card_keys_at(now_unix_seconds)?;
        Ok(items
            .iter()
            .filter_map(|item| {
                deferred
                    .contains(item.datum_key())
                    .then_some(item.datum_key().clone())
            })
            .collect())
    }

    pub(crate) fn load_deferred_study_item_keys(
        &self,
        items: &[StudyItem],
    ) -> Result<HashSet<DatumKey>, String> {
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        self.load_deferred_study_item_keys_at(items, now_unix_seconds)
    }

    fn available_flagged_review_queue(
        &self,
        items: &[StudyItem],
        now_unix_seconds: i64,
    ) -> Result<Vec<usize>, String> {
        let flagged = self.load_flagged_study_item_keys(items)?;
        let suspended = self.load_suspended_study_item_keys(items)?;
        let deferred = self.load_deferred_study_item_keys_at(items, now_unix_seconds)?;
        Ok(items
            .iter()
            .enumerate()
            .filter_map(|(index, item)| {
                (flagged.contains(item.datum_key())
                    && !suspended.contains(item.datum_key())
                    && !deferred.contains(item.datum_key()))
                .then_some(index)
            })
            .collect())
    }

    pub(crate) fn available_flagged_card_count_at(
        &self,
        now_unix_seconds: i64,
    ) -> Result<usize, String> {
        let items = self.study_items()?;
        Ok(self
            .available_flagged_review_queue(&items, now_unix_seconds)?
            .len())
    }

    pub(crate) fn available_flagged_card_count(&self) -> Result<usize, String> {
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        self.available_flagged_card_count_at(now_unix_seconds)
    }

    pub(crate) fn active_card_count(&self) -> Result<usize, String> {
        let items = self.study_items()?;
        let now_unix_seconds = current_time_and_seed()
            .map(|(now_unix_seconds, _)| now_unix_seconds)
            .unwrap_or(0);
        Ok(self
            .active_study_item_indices_at(&items, now_unix_seconds)?
            .len())
    }

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

    fn next_available_card_id(&self) -> Result<u64, String> {
        let mut used_ids: HashSet<u64> = self.data.iter().filter_map(Datum::card_id).collect();
        for card_id in self.store.used_card_ids().map_err(|error| {
            self.localizer
                .could_not_build_review_queue(&error.to_string())
        })? {
            used_ids.insert(card_id);
        }

        Ok(next_unused_card_id(&used_ids))
    }
}
