use std::collections::HashMap; use rusqlite::Connection; use serde::Serialize; use crate::crypto::encryption::{decrypt_data_with_user_key, encrypt_data_with_user_key, hash_element}; use crate::crypto::key_manager::get_user_encryption_key; use crate::domains::book::repo as book_repo; use crate::domains::spell::repo; use crate::domains::spell_tag::repo as spell_tag_repo; use crate::domains::tombstone::repo as tombstone_repo; use crate::error::{AppError, AppResult}; use crate::helpers::{create_unique_id, timestamp_in_seconds}; use crate::shared::types::Lang; #[derive(Serialize)] pub struct SpellTagProps { pub id: String, pub name: String, pub color: Option, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SpellProps { pub id: String, pub name: String, pub description: String, pub appearance: String, pub tags: Vec, pub power_level: Option, pub components: Option, pub limitations: Option, pub notes: Option, pub series_spell_id: Option, } #[derive(Serialize)] #[serde(rename_all = "camelCase")] pub struct SpellListItem { pub id: String, pub name: String, pub description: String, pub tags: Vec, pub series_spell_id: Option, } #[derive(Serialize)] pub struct SpellListResponse { pub enabled: bool, pub spells: Vec, pub tags: Vec, } /// Retrieves all spell tags for a specific book. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `book_id` - The unique identifier of the book /// * `lang` - The language for error messages ("fr" or "en") /// Returns an array of spell tag props. pub fn get_spell_tags(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult> { let user_key: String = get_user_encryption_key(user_id)?; let spell_tags: Vec = spell_tag_repo::fetch_spell_tags(conn, user_id, book_id, lang)?; let mut result: Vec = Vec::with_capacity(spell_tags.len()); for tag in spell_tags { result.push(SpellTagProps { id: tag.tag_id, name: decrypt_data_with_user_key(&tag.name, &user_key)?, color: tag.color, }); } Ok(result) } /// Adds a new spell tag to a book. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `book_id` - The unique identifier of the book /// * `name` - The name of the tag /// * `color` - The optional color hex code /// * `existing_tag_id` - Optional existing tag ID for sync /// * `lang` - The language for error messages ("fr" or "en") /// Returns the created spell tag props. pub fn add_spell_tag( conn: &Connection, user_id: &str, book_id: &str, name: &str, color: Option<&str>, existing_tag_id: Option<&str>, lang: Lang, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let tag_id: String = create_unique_id(existing_tag_id); let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?; let name_hash: String = hash_element(name); let last_update: i64 = timestamp_in_seconds(); spell_tag_repo::insert_spell_tag(conn, &tag_id, book_id, user_id, &encrypted_name, &name_hash, color, last_update, lang)?; Ok(SpellTagProps { id: tag_id, name: name.to_string(), color: color.map(|c| c.to_string()), }) } /// Updates an existing spell tag. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `tag_id` - The unique identifier of the tag /// * `name` - The new name of the tag /// * `color` - The new optional color hex code /// * `lang` - The language for error messages ("fr" or "en") /// Returns true if the update was successful. pub fn update_spell_tag( conn: &Connection, user_id: &str, tag_id: &str, name: &str, color: Option<&str>, lang: Lang, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?; let name_hash: String = hash_element(name); let last_update: i64 = timestamp_in_seconds(); spell_tag_repo::update_spell_tag(conn, user_id, tag_id, &encrypted_name, &name_hash, color, last_update, lang) } /// Deletes a spell tag and removes its references from all spells in the book. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `book_id` - The unique identifier of the book /// * `tag_id` - The unique identifier of the tag to delete /// * `deleted_at` - The timestamp of deletion /// * `lang` - The language for error messages ("fr" or "en") /// Returns true if the deletion was successful. pub fn delete_spell_tag( conn: &Connection, user_id: &str, book_id: &str, tag_id: &str, deleted_at: i64, lang: Lang, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let spells: Vec = repo::fetch_spells(conn, user_id, book_id, lang)?; let last_update: i64 = timestamp_in_seconds(); for spell in &spells { let decrypted_tags: Option = if let Some(ref tags) = spell.tags { Some(decrypt_data_with_user_key(tags, &user_key)?) } else { None }; let tags_array: Vec = match decrypted_tags { Some(ref tags_str) => serde_json::from_str(tags_str).unwrap_or_default(), None => Vec::new(), }; if tags_array.contains(&tag_id.to_string()) { let updated_tags: Vec<&String> = tags_array.iter().filter(|t| t.as_str() != tag_id).collect(); let serialized_tags: String = serde_json::to_string(&updated_tags).unwrap_or_else(|_| "[]".to_string()); let encrypted_tags: String = encrypt_data_with_user_key(&serialized_tags, &user_key)?; repo::update_spell_tags(conn, user_id, &spell.spell_id, &encrypted_tags, last_update, lang)?; } } let deleted: bool = spell_tag_repo::delete_spell_tag(conn, user_id, tag_id, lang)?; if deleted { let removal_id: String = create_unique_id(None); tombstone_repo::insert(conn, &removal_id, "book_spell_tags", tag_id, Some(book_id), user_id, deleted_at, lang)?; } Ok(deleted) } /// Retrieves the spell list with tags for a specific book. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `book_id` - The unique identifier of the book /// * `lang` - The language for error messages ("fr" or "en") /// Returns the spell list response with enabled status, spells, and tags. pub fn get_spell_list(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let book_tools: Option = book_repo::fetch_book_tools(conn, user_id, book_id, lang)?; let enabled: bool = book_tools.map_or(false, |bt| bt.spells_enabled == 1); let spell_tags: Vec = spell_tag_repo::fetch_spell_tags(conn, user_id, book_id, lang)?; let mut tags: Vec = Vec::with_capacity(spell_tags.len()); let mut tag_map: HashMap)> = HashMap::new(); for tag in spell_tags { let decrypted_name: String = decrypt_data_with_user_key(&tag.name, &user_key)?; tag_map.insert(tag.tag_id.clone(), (decrypted_name.clone(), tag.color.clone())); tags.push(SpellTagProps { id: tag.tag_id, name: decrypted_name, color: tag.color, }); } let spell_results: Vec = repo::fetch_spells(conn, user_id, book_id, lang)?; let mut spells: Vec = Vec::with_capacity(spell_results.len()); for spell in spell_results { let decrypted_name: String = decrypt_data_with_user_key(&spell.name, &user_key)?; let decrypted_description: Option = if let Some(ref description) = spell.description { Some(decrypt_data_with_user_key(description, &user_key)?) } else { None }; let decrypted_tags: Option = if let Some(ref tags_str) = spell.tags { Some(decrypt_data_with_user_key(tags_str, &user_key)?) } else { None }; let tag_ids: Vec = match decrypted_tags { Some(ref tags_str) => serde_json::from_str(tags_str).unwrap_or_default(), None => Vec::new(), }; let resolved_tags: Vec = tag_ids .iter() .filter_map(|tag_id| { tag_map.get(tag_id).map(|(name, color)| SpellTagProps { id: tag_id.clone(), name: name.clone(), color: color.clone(), }) }) .collect(); let truncated_description: String = match decrypted_description { Some(ref desc) if desc.len() > 150 => format!("{}...", &desc[..150]), Some(ref desc) => desc.clone(), None => String::new(), }; spells.push(SpellListItem { id: spell.spell_id, name: decrypted_name, description: truncated_description, tags: resolved_tags, series_spell_id: spell.series_spell_id, }); } Ok(SpellListResponse { enabled, spells, tags }) } /// Retrieves the full details of a specific spell. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `spell_id` - The unique identifier of the spell /// * `lang` - The language for error messages ("fr" or "en") /// Returns the spell props with all details. pub fn get_spell_detail(conn: &Connection, user_id: &str, spell_id: &str, lang: Lang) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let spell: repo::SpellResult = repo::fetch_spell_by_id(conn, user_id, spell_id, lang)? .ok_or_else(|| AppError::Internal(if lang == Lang::Fr { "Sort non trouvé.".to_string() } else { "Spell not found.".to_string() }))?; let decrypted_name: String = decrypt_data_with_user_key(&spell.name, &user_key)?; let decrypted_description: String = if let Some(ref description) = spell.description { if description.is_empty() { String::new() } else { decrypt_data_with_user_key(description, &user_key)? } } else { String::new() }; let decrypted_appearance: String = if let Some(ref appearance) = spell.appearance { if appearance.is_empty() { String::new() } else { decrypt_data_with_user_key(appearance, &user_key)? } } else { String::new() }; let decrypted_tags: Option = if let Some(ref tags) = spell.tags { if tags.is_empty() { None } else { Some(decrypt_data_with_user_key(tags, &user_key)?) } } else { None }; let tag_ids: Vec = match decrypted_tags { Some(ref tags_str) => serde_json::from_str(tags_str).unwrap_or_default(), None => Vec::new(), }; Ok(SpellProps { id: spell.spell_id, name: decrypted_name, description: decrypted_description, appearance: decrypted_appearance, tags: tag_ids, power_level: if let Some(ref power_level) = spell.power_level { Some(decrypt_data_with_user_key(power_level, &user_key)?) } else { None }, components: if let Some(ref components) = spell.components { Some(decrypt_data_with_user_key(components, &user_key)?) } else { None }, limitations: if let Some(ref limitations) = spell.limitations { Some(decrypt_data_with_user_key(limitations, &user_key)?) } else { None }, notes: if let Some(ref notes) = spell.notes { Some(decrypt_data_with_user_key(notes, &user_key)?) } else { None }, series_spell_id: spell.series_spell_id, }) } /// Adds a new spell to a book. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `book_id` - The unique identifier of the book /// * `name` - The name of the spell /// * `description` - The description of the spell /// * `appearance` - The appearance of the spell /// * `tags` - The tag IDs array /// * `power_level` - The optional power level /// * `components` - The optional components /// * `limitations` - The optional limitations /// * `notes` - The optional notes /// * `existing_spell_id` - Optional existing spell ID for sync /// * `lang` - The language for error messages ("fr" or "en") /// * `series_spell_id` - The optional series spell identifier /// Returns the created spell props. pub fn add_spell( conn: &Connection, user_id: &str, book_id: &str, name: &str, description: &str, appearance: &str, tags: Vec, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>, notes: Option<&str>, existing_spell_id: Option<&str>, lang: Lang, series_spell_id: Option<&str>, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let spell_id: String = create_unique_id(existing_spell_id); let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?; let name_hash: String = hash_element(name); let encrypted_description: String = encrypt_data_with_user_key(description, &user_key)?; let encrypted_appearance: String = encrypt_data_with_user_key(appearance, &user_key)?; let serialized_tags: String = serde_json::to_string(&tags).unwrap_or_else(|_| "[]".to_string()); let encrypted_tags: String = encrypt_data_with_user_key(&serialized_tags, &user_key)?; let encrypted_power_level: Option = if let Some(power_level_val) = power_level { Some(encrypt_data_with_user_key(power_level_val, &user_key)?) } else { None }; let encrypted_components: Option = if let Some(components_val) = components { Some(encrypt_data_with_user_key(components_val, &user_key)?) } else { None }; let encrypted_limitations: Option = if let Some(limitations_val) = limitations { Some(encrypt_data_with_user_key(limitations_val, &user_key)?) } else { None }; let encrypted_notes: Option = if let Some(notes_val) = notes { Some(encrypt_data_with_user_key(notes_val, &user_key)?) } else { None }; let last_update: i64 = timestamp_in_seconds(); repo::insert_spell( conn, &spell_id, book_id, user_id, &encrypted_name, &name_hash, Some(&encrypted_description), Some(&encrypted_appearance), &encrypted_tags, encrypted_power_level.as_deref(), encrypted_components.as_deref(), encrypted_limitations.as_deref(), encrypted_notes.as_deref(), last_update, lang, series_spell_id, )?; Ok(SpellProps { id: spell_id, name: name.to_string(), description: description.to_string(), appearance: appearance.to_string(), tags, power_level: power_level.map(|s| s.to_string()), components: components.map(|s| s.to_string()), limitations: limitations.map(|s| s.to_string()), notes: notes.map(|s| s.to_string()), series_spell_id: series_spell_id.map(|s| s.to_string()), }) } /// Updates an existing spell. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `spell_id` - The unique identifier of the spell /// * `name` - The name of the spell /// * `description` - The description of the spell /// * `appearance` - The appearance of the spell /// * `tags` - The tag IDs array /// * `power_level` - The optional power level /// * `components` - The optional components /// * `limitations` - The optional limitations /// * `notes` - The optional notes /// * `lang` - The language for error messages ("fr" or "en") /// * `series_spell_id` - The optional series spell identifier /// Returns true if the update was successful. pub fn update_spell( conn: &Connection, user_id: &str, spell_id: &str, name: &str, description: &str, appearance: &str, tags: Vec, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>, notes: Option<&str>, lang: Lang, series_spell_id: Option<&str>, ) -> AppResult { let user_key: String = get_user_encryption_key(user_id)?; let encrypted_name: String = encrypt_data_with_user_key(name, &user_key)?; let name_hash: String = hash_element(name); let encrypted_description: String = encrypt_data_with_user_key(description, &user_key)?; let encrypted_appearance: String = encrypt_data_with_user_key(appearance, &user_key)?; let serialized_tags: String = serde_json::to_string(&tags).unwrap_or_else(|_| "[]".to_string()); let encrypted_tags: String = encrypt_data_with_user_key(&serialized_tags, &user_key)?; let encrypted_power_level: Option = if let Some(power_level_val) = power_level { Some(encrypt_data_with_user_key(power_level_val, &user_key)?) } else { None }; let encrypted_components: Option = if let Some(components_val) = components { Some(encrypt_data_with_user_key(components_val, &user_key)?) } else { None }; let encrypted_limitations: Option = if let Some(limitations_val) = limitations { Some(encrypt_data_with_user_key(limitations_val, &user_key)?) } else { None }; let encrypted_notes: Option = if let Some(notes_val) = notes { Some(encrypt_data_with_user_key(notes_val, &user_key)?) } else { None }; let last_update: i64 = timestamp_in_seconds(); repo::update_spell( conn, user_id, spell_id, &encrypted_name, &name_hash, Some(&encrypted_description), Some(&encrypted_appearance), &encrypted_tags, encrypted_power_level.as_deref(), encrypted_components.as_deref(), encrypted_limitations.as_deref(), encrypted_notes.as_deref(), last_update, lang, series_spell_id, ) } /// Deletes a spell. /// * `conn` - Database connection /// * `user_id` - The unique identifier of the user /// * `book_id` - The unique identifier of the book /// * `spell_id` - The unique identifier of the spell /// * `deleted_at` - The timestamp of deletion /// * `lang` - The language for error messages ("fr" or "en") /// Returns true if the deletion was successful. pub fn delete_spell(conn: &Connection, user_id: &str, book_id: &str, spell_id: &str, deleted_at: i64, lang: Lang) -> AppResult { let deleted: bool = repo::delete_spell(conn, user_id, spell_id, lang)?; if deleted { let removal_id: String = create_unique_id(None); tombstone_repo::insert(conn, &removal_id, "book_spells", spell_id, Some(book_id), user_id, deleted_at, lang)?; } Ok(deleted) }