use std::path::PathBuf;

use blake3::hash;
use lunar_lib::{
    database::{DatabaseEntry, Db, Entry, Id, IdExt, TransactionError},
    define_db,
    iterator_ext::{IntoIteratorExtensions, IteratorExtensions},
    log::trace,
    paths::data_dir,
};

mod tx_extensions;
pub(crate) use tx_extensions::*;

#[cfg(debug_assertions)]
pub mod validator;

define_db!(Library {
    fn path() -> Option<PathBuf> {
        Some(data_dir().join("library_data"))
    }
});

pub trait SeleneIdExt {
    /// Turns an ID into its `name:id` counterpart, eg. `track:<id>`
    fn to_selene_id(&self) -> String;

    /// Gets an ID from the hash of a string. This will not work for tracks, which require the hash of the source file
    fn from_string_hash(string: impl AsRef<str>) -> Self;
}

impl<T: DatabaseEntry> SeleneIdExt for Id<T> {
    fn to_selene_id(&self) -> String {
        format!("{}:{}", T::TREE_NAME, self)
    }

    fn from_string_hash(string: impl AsRef<str>) -> Self {
        let hash = hash(string.as_ref().to_lowercase().as_bytes());
        Self::from(*hash.as_bytes())
    }
}

pub trait Searchable: DatabaseEntry {
    const SEARCH_INDEX: &'static str;

    fn search_name(&self) -> Option<&str>;

    fn build_search_index(db: &Db<Self::DbInner>) -> Result<(), TransactionError> {
        db.index::<Self>(Self::SEARCH_INDEX).rebuild(|tree, entry| {
            if let Some(title) = entry.search_name().map(str::to_ascii_lowercase) {
                let mut key = [0u8; 8];
                let bytes = title.as_bytes();
                let len = bytes.len().min(8);
                key[..len].copy_from_slice(&bytes[..len]);

                tree.fetch_and_update(key, |old| match old {
                    Some(v) => {
                        let mut new_value = v.to_vec();
                        new_value.extend_from_slice(&*entry.id());
                        Some(new_value)
                    }
                    None => Some(entry.id().to_vec()),
                })?;
            }

            Ok(())
        })
    }

    fn search(
        db: &Db<Self::DbInner>,
        query: &str,
        limit: usize,
        offset: usize,
    ) -> Result<Vec<Entry<Self>>, TransactionError> {
        trace!(
            "Searching index {} with query '{query}'",
            Self::SEARCH_INDEX
        );
        let query = query.to_ascii_lowercase();
        let len = query.len().min(8);
        let key = &query.as_bytes()[..len];

        let mut items = db
            .index::<Self>(Self::SEARCH_INDEX)
            .scan_prefix(key)
            .try_map(|i| -> Result<Vec<Id<Self>>, TransactionError> {
                let (_, v) = i.map_err(TransactionError::from)?;
                Ok(v.chunks_exact(32)
                    .map(|chunk| Id::<Self>::from(<[u8; 32]>::try_from(chunk).unwrap()))
                    .collect())
            })?
            .flatten()
            .db_get(db)?;

        items.retain(|item| {
            item.search_name()
                .expect("Items without names cant be indexed")
                .to_ascii_lowercase()
                .starts_with(&query)
        });

        {
            let result = items
                .iter()
                .map(|t| t.search_name().unwrap())
                .join_to_string(", ");
            trace!("Search found: {result}");
        }

        Ok(items.into_iter().skip(offset).take(limit).to_vec())
    }
}

#[cfg(test)]
pub(crate) mod tests {
    use std::{path::PathBuf, sync::LazyLock};

    use lunar_lib::{database::Db, define_db};

    pub(crate) static TEST_DB: LazyLock<Db<TestDb>> = LazyLock::new(|| Db::open().unwrap());

    define_db!(TestDb {
        fn path() -> Option<PathBuf> {
            None
        }
    });
}
