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<String>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpellProps {
    pub id: String,
    pub name: String,
    pub description: String,
    pub appearance: String,
    pub tags: Vec<String>,
    pub power_level: Option<String>,
    pub components: Option<String>,
    pub limitations: Option<String>,
    pub notes: Option<String>,
    pub series_spell_id: Option<String>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpellListItem {
    pub id: String,
    pub name: String,
    pub description: String,
    pub tags: Vec<SpellTagProps>,
    pub series_spell_id: Option<String>,
}

#[derive(Serialize)]
pub struct SpellListResponse {
    pub enabled: bool,
    pub spells: Vec<SpellListItem>,
    pub tags: Vec<SpellTagProps>,
}

/// 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<Vec<SpellTagProps>> {
    let user_key: String = get_user_encryption_key(user_id)?;
    let spell_tags: Vec<spell_tag_repo::SpellTagResult> = spell_tag_repo::fetch_spell_tags(conn, user_id, book_id, lang)?;

    let mut result: Vec<SpellTagProps> = 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<SpellTagProps> {
    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<bool> {
    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<bool> {
    let user_key: String = get_user_encryption_key(user_id)?;
    let spells: Vec<repo::SpellResult> = repo::fetch_spells(conn, user_id, book_id, lang)?;
    let last_update: i64 = timestamp_in_seconds();

    for spell in &spells {
        let decrypted_tags: Option<String> = if let Some(ref tags) = spell.tags {
            Some(decrypt_data_with_user_key(tags, &user_key)?)
        } else {
            None
        };

        let tags_array: Vec<String> = 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<SpellListResponse> {
    let user_key: String = get_user_encryption_key(user_id)?;

    let book_tools: Option<book_repo::BookToolsTable> = 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::SpellTagResult> = spell_tag_repo::fetch_spell_tags(conn, user_id, book_id, lang)?;
    let mut tags: Vec<SpellTagProps> = Vec::with_capacity(spell_tags.len());
    let mut tag_map: HashMap<String, (String, Option<String>)> = 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::SpellResult> = repo::fetch_spells(conn, user_id, book_id, lang)?;
    let mut spells: Vec<SpellListItem> = 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<String> = if let Some(ref description) = spell.description {
            Some(decrypt_data_with_user_key(description, &user_key)?)
        } else {
            None
        };
        let decrypted_tags: Option<String> = if let Some(ref tags_str) = spell.tags {
            Some(decrypt_data_with_user_key(tags_str, &user_key)?)
        } else {
            None
        };

        let tag_ids: Vec<String> = match decrypted_tags {
            Some(ref tags_str) => serde_json::from_str(tags_str).unwrap_or_default(),
            None => Vec::new(),
        };

        let resolved_tags: Vec<SpellTagProps> = 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.chars().count() > 150 => format!("{}...", desc.chars().take(150).collect::<String>()),
            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<SpellProps> {
    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<String> = 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<String> = 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<String>, 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<SpellProps> {
    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<String> = 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<String> = if let Some(components_val) = components { Some(encrypt_data_with_user_key(components_val, &user_key)?) } else { None };
    let encrypted_limitations: Option<String> = if let Some(limitations_val) = limitations { Some(encrypt_data_with_user_key(limitations_val, &user_key)?) } else { None };
    let encrypted_notes: Option<String> = 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<String>, power_level: Option<&str>, components: Option<&str>, limitations: Option<&str>,
    notes: Option<&str>, lang: Lang, series_spell_id: Option<&str>,
) -> AppResult<bool> {
    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<String> = 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<String> = if let Some(components_val) = components { Some(encrypt_data_with_user_key(components_val, &user_key)?) } else { None };
    let encrypted_limitations: Option<String> = if let Some(limitations_val) = limitations { Some(encrypt_data_with_user_key(limitations_val, &user_key)?) } else { None };
    let encrypted_notes: Option<String> = 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<bool> {
    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)
}
