use std::collections::HashSet;
use std::path::{Path, PathBuf};

#[cfg(test)]
use crate::app_config::load_app_config;
use crate::file_ops::portable_path;
use cosimo::{Datum, DatumKey, DeckMetadata, ReviewStoreError, SqliteReviewStore};

pub(crate) const DEFAULT_DECK_PATH: &str = "decks/deck.cosimo-deck";

#[cfg(test)]
pub(crate) fn startup_deck_path(config_path: impl AsRef<Path>) -> PathBuf {
    let default_path = default_deck_path();
    let Some(saved_path) = load_app_config(config_path)
        .ok()
        .and_then(|config| config.last_open_deck_path)
    else {
        return default_path;
    };

    startup_deck_path_from_saved(default_path, saved_path)
}

pub(crate) fn startup_deck_path_from_saved(default_path: PathBuf, saved_path: PathBuf) -> PathBuf {
    if saved_path.exists() {
        saved_path
    } else {
        default_path
    }
}

pub(crate) fn default_deck_path() -> PathBuf {
    portable_path(DEFAULT_DECK_PATH)
}

pub(crate) fn review_db_path_for_deck(deck_path: &Path) -> PathBuf {
    let parent = deck_path.parent().unwrap_or_else(|| Path::new("."));
    let stem = deck_path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .filter(|stem| !stem.is_empty())
        .unwrap_or("deck");

    parent.join(format!("{stem}.reviews.sqlite3"))
}

pub(crate) fn enumerate_deck_cards(
    data: Vec<Datum>,
    store: &SqliteReviewStore,
    metadata: &mut DeckMetadata,
) -> Result<(Vec<Datum>, bool), ReviewStoreError> {
    let mut changed = false;
    let had_next_card_id_metadata = metadata.next_card_id().is_some();
    // Old beta decks did not carry numeric card ids. Assign ids once, but
    // leave review-store key migration until after the rewritten deck has been
    // durably saved.
    let mut used_ids = card_ids_used_by_deck_and_store(&data, store)?;

    let mut enumerated = Vec::with_capacity(data.len());
    for datum in data {
        if datum.card_id().is_some() {
            enumerated.push(datum);
            continue;
        }

        let card_id = next_monotonic_card_id(&used_ids, metadata)?;
        used_ids.insert(card_id);
        advance_next_card_id_metadata(metadata, card_id)?;
        let upgraded = datum.with_card_id(card_id);
        enumerated.push(upgraded);
        changed = true;
    }
    if (changed || had_next_card_id_metadata) && ensure_next_card_id_metadata(metadata, &used_ids)?
    {
        changed = true;
    }

    Ok((enumerated, changed))
}

pub(crate) fn migrate_pending_legacy_content_keys(
    data: &[Datum],
    store: &SqliteReviewStore,
) -> Result<usize, ReviewStoreError> {
    let mut updated = 0;
    for (old_key, new_key) in pending_legacy_content_key_migrations(data, store)? {
        updated += store.migrate_datum_key(&old_key, &new_key)?;
    }
    Ok(updated)
}

fn pending_legacy_content_key_migrations(
    data: &[Datum],
    store: &SqliteReviewStore,
) -> Result<Vec<(DatumKey, DatumKey)>, ReviewStoreError> {
    let mut migrations = Vec::new();
    for datum in data {
        let old_key = DatumKey::for_legacy_content(datum);
        let new_key = DatumKey::for_datum(datum);
        if old_key == new_key {
            continue;
        }
        if store.datum_key_has_storage(&old_key)? && !store.datum_key_has_storage(&new_key)? {
            migrations.push((old_key, new_key));
        }
    }
    Ok(migrations)
}

pub(crate) fn deck_needs_card_ids(data: &[Datum]) -> bool {
    data.iter().any(|datum| datum.card_id().is_none())
}

pub(crate) fn deck_needs_next_card_id_metadata_repair(
    data: &[Datum],
    store: &SqliteReviewStore,
    metadata: &DeckMetadata,
) -> Result<bool, ReviewStoreError> {
    let used_ids = card_ids_used_by_deck_and_store(data, store)?;
    let required_next_card_id = next_after_highest_card_id(&used_ids)?;
    Ok(metadata
        .next_card_id()
        .is_some_and(|current| current < required_next_card_id))
}

pub(crate) fn card_ids_used_by_deck_and_store(
    data: &[Datum],
    store: &SqliteReviewStore,
) -> Result<HashSet<u64>, ReviewStoreError> {
    let mut used_ids: HashSet<u64> = store.used_card_ids()?.into_iter().collect();
    for card_id in data.iter().filter_map(Datum::card_id) {
        used_ids.insert(card_id);
    }
    Ok(used_ids)
}

pub(crate) fn next_monotonic_card_id(
    used_ids: &HashSet<u64>,
    metadata: &DeckMetadata,
) -> Result<u64, ReviewStoreError> {
    let next_after_used = next_after_highest_card_id(used_ids)?;
    let candidate = metadata.next_card_id().unwrap_or(1).max(next_after_used);
    next_after_card_id(candidate)?;
    Ok(candidate)
}

pub(crate) fn ensure_next_card_id_metadata(
    metadata: &mut DeckMetadata,
    used_ids: &HashSet<u64>,
) -> Result<bool, ReviewStoreError> {
    let next_card_id = next_after_highest_card_id(used_ids)?;
    Ok(metadata.ensure_next_card_id_at_least(next_card_id))
}

pub(crate) fn advance_next_card_id_metadata(
    metadata: &mut DeckMetadata,
    assigned_card_id: u64,
) -> Result<bool, ReviewStoreError> {
    let next_card_id = next_after_card_id(assigned_card_id)?;
    Ok(metadata.ensure_next_card_id_at_least(next_card_id))
}

fn next_after_highest_card_id(used_ids: &HashSet<u64>) -> Result<u64, ReviewStoreError> {
    next_after_card_id(used_ids.iter().copied().max().unwrap_or(0))
}

fn next_after_card_id(card_id: u64) -> Result<u64, ReviewStoreError> {
    card_id.checked_add(1).ok_or_else(|| {
        ReviewStoreError::ConsistencyCheckFailed("card ID space is exhausted".to_string())
    })
}
