use rusqlite::Connection;
use serde::{Deserialize, Serialize};

use crate::crypto::encryption::{encrypt_data_with_user_key, decrypt_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::location::repo;
use crate::domains::tombstone::repo as tombstone_repo;
use crate::error::AppResult;
use crate::helpers::{create_unique_id, timestamp_in_seconds};
use crate::shared::types::Lang;

#[derive(Serialize, Deserialize)]
pub struct SubElement {
    pub id: String,
    pub name: String,
    pub description: String,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Element {
    pub id: String,
    pub name: String,
    pub description: String,
    pub sub_elements: Vec<SubElement>,
}

#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LocationProps {
    pub id: String,
    pub name: String,
    pub elements: Vec<Element>,
    pub series_location_id: Option<String>,
}

#[derive(Serialize)]
pub struct LocationListResponse {
    pub locations: Vec<LocationProps>,
    pub enabled: bool,
}

/// Retrieves all locations for a given user and book.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `book_id` - The book's unique identifier
/// * `lang` - The language for error messages
/// Returns LocationListResponse containing an array of locations and enabled flag.
pub fn get_all_locations(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<LocationListResponse> {
    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, |book_tools_row| book_tools_row.locations_enabled == 1);

    let location_records: Vec<repo::LocationQueryResult> = repo::get_location(conn, user_id, book_id, lang)?;
    if location_records.is_empty() {
        return Ok(LocationListResponse { locations: vec![], enabled });
    }

    let user_key: String = get_user_encryption_key(user_id)?;
    let mut location_array: Vec<LocationProps> = Vec::new();

    for record in &location_records {
        let location_index: Option<usize> = location_array.iter().position(|loc| loc.id == record.loc_id);

        let location_idx: usize = match location_index {
            Some(idx) => idx,
            None => {
                let decrypted_name: String = decrypt_data_with_user_key(&record.loc_name, &user_key)?;
                location_array.push(LocationProps {
                    id: record.loc_id.clone(),
                    name: decrypted_name,
                    elements: vec![],
                    series_location_id: record.series_location_id.clone(),
                });
                location_array.len() - 1
            }
        };

        if let Some(ref element_id) = record.element_id {
            let element_index: Option<usize> = location_array[location_idx].elements.iter().position(|elem| elem.id == *element_id);

            let element_idx: usize = match element_index {
                Some(idx) => idx,
                None => {
                    let decrypted_name: String = match record.element_name.as_deref() {
                        Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
                        _ => String::new(),
                    };
                    let decrypted_description: String = match record.element_description.as_deref() {
                        Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
                        _ => String::new(),
                    };
                    location_array[location_idx].elements.push(Element {
                        id: element_id.clone(),
                        name: decrypted_name,
                        description: decrypted_description,
                        sub_elements: vec![],
                    });
                    location_array[location_idx].elements.len() - 1
                }
            };

            if let Some(ref sub_element_id) = record.sub_element_id {
                let sub_element_exists: bool = location_array[location_idx].elements[element_idx]
                    .sub_elements
                    .iter()
                    .any(|sub| sub.id == *sub_element_id);

                if !sub_element_exists {
                    let decrypted_name: String = match record.sub_elem_name.as_deref() {
                        Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
                        _ => String::new(),
                    };
                    let decrypted_description: String = match record.sub_elem_description.as_deref() {
                        Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
                        _ => String::new(),
                    };
                    location_array[location_idx].elements[element_idx].sub_elements.push(SubElement {
                        id: sub_element_id.clone(),
                        name: decrypted_name,
                        description: decrypted_description,
                    });
                }
            }
        }
    }

    Ok(LocationListResponse { locations: location_array, enabled })
}

/// Adds a new location section for a book.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `location_name` - The name of the location to create
/// * `book_id` - The book's unique identifier
/// * `lang` - The language for error messages
/// * `existing_location_id` - Optional existing location ID to use instead of generating a new one
/// * `series_location_id` - The series location ID to link (optional)
/// Returns the ID of the created location.
pub fn add_location_section(
    conn: &Connection, user_id: &str, location_name: &str, book_id: &str, lang: Lang,
    existing_location_id: Option<&str>, series_location_id: Option<&str>,
) -> AppResult<String> {
    let user_key: String = get_user_encryption_key(user_id)?;
    let hashed_name: String = hash_element(location_name);
    let encrypted_name: String = encrypt_data_with_user_key(location_name, &user_key)?;
    let location_id: String = create_unique_id(existing_location_id);
    let last_update: i64 = timestamp_in_seconds();
    repo::insert_location(conn, user_id, &location_id, book_id, &encrypted_name, &hashed_name, last_update, series_location_id, lang)
}

/// Adds a new element to a location.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `location_id` - The parent location's unique identifier
/// * `element_name` - The name of the element to create
/// * `lang` - The language for error messages
/// * `existing_element_id` - Optional existing element ID to use instead of generating a new one
/// Returns the ID of the created element.
pub fn add_location_element(
    conn: &Connection, user_id: &str, location_id: &str, element_name: &str, lang: Lang,
    existing_element_id: Option<&str>,
) -> AppResult<String> {
    let user_key: String = get_user_encryption_key(user_id)?;
    let hashed_name: String = hash_element(element_name);
    let encrypted_name: String = encrypt_data_with_user_key(element_name, &user_key)?;
    let element_id: String = create_unique_id(existing_element_id);
    let last_update: i64 = timestamp_in_seconds();
    repo::insert_location_element(conn, user_id, &element_id, location_id, &encrypted_name, &hashed_name, last_update, lang)
}

/// Adds a new sub-element to a location element.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `element_id` - The parent element's unique identifier
/// * `sub_element_name` - The name of the sub-element to create
/// * `lang` - The language for error messages
/// * `existing_sub_element_id` - Optional existing sub-element ID to use instead of generating a new one
/// Returns the ID of the created sub-element.
pub fn add_location_sub_element(
    conn: &Connection, user_id: &str, element_id: &str, sub_element_name: &str, lang: Lang,
    existing_sub_element_id: Option<&str>,
) -> AppResult<String> {
    let user_key: String = get_user_encryption_key(user_id)?;
    let hashed_name: String = hash_element(sub_element_name);
    let encrypted_name: String = encrypt_data_with_user_key(sub_element_name, &user_key)?;
    let sub_element_id: String = create_unique_id(existing_sub_element_id);
    let last_update: i64 = timestamp_in_seconds();
    repo::insert_location_sub_element(conn, user_id, &sub_element_id, element_id, &encrypted_name, &hashed_name, last_update, lang)
}

/// Updates multiple location sections along with their elements and sub-elements.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `locations` - Array of location properties to update
/// * `lang` - The language for response messages
/// Returns an object indicating success and a localized message.
pub fn update_location_section(
    conn: &Connection, user_id: &str, locations: &[LocationProps], lang: Lang,
) -> AppResult<(bool, String)> {
    let user_key: String = get_user_encryption_key(user_id)?;

    for location in locations {
        let hashed_location_name: String = hash_element(&location.name);
        let encrypted_location_name: String = encrypt_data_with_user_key(&location.name, &user_key)?;
        let last_update: i64 = timestamp_in_seconds();
        repo::update_location_section(conn, user_id, &location.id, &encrypted_location_name, &hashed_location_name, last_update, lang)?;

        for element in &location.elements {
            let hashed_element_name: String = hash_element(&element.name);
            let encrypted_element_name: String = encrypt_data_with_user_key(&element.name, &user_key)?;
            let encrypted_element_description: String = if element.description.is_empty() { String::new() } else { encrypt_data_with_user_key(&element.description, &user_key)? };
            let last_update: i64 = timestamp_in_seconds();
            repo::update_location_element(conn, user_id, &element.id, &encrypted_element_name, &hashed_element_name, &encrypted_element_description, last_update, lang)?;

            for sub_element in &element.sub_elements {
                let hashed_sub_element_name: String = hash_element(&sub_element.name);
                let encrypted_sub_element_name: String = encrypt_data_with_user_key(&sub_element.name, &user_key)?;
                let encrypted_sub_element_description: String = if sub_element.description.is_empty() { String::new() } else { encrypt_data_with_user_key(&sub_element.description, &user_key)? };
                let last_update: i64 = timestamp_in_seconds();
                repo::update_location_sub_element(conn, user_id, &sub_element.id, &encrypted_sub_element_name, &hashed_sub_element_name, &encrypted_sub_element_description, last_update, lang)?;
            }
        }
    }

    let message: String = if lang == Lang::Fr { "Les sections ont été mis à jour.".to_string() } else { "Sections have been updated.".to_string() };
    Ok((true, message))
}

/// Updates a location section with optional name change and series link.
/// * `conn` - Database connection
/// * `user_id` - The unique identifier of the user
/// * `section_id` - The unique identifier of the section
/// * `section_name` - The new name (optional)
/// * `series_location_id` - The series location ID to link (optional, None to unlink)
/// * `lang` - The language for error messages
/// Returns true if the update was successful.
pub fn update_section_with_series_link(
    conn: &Connection, user_id: &str, section_id: &str, section_name: Option<&str>,
    series_location_id: Option<&str>, lang: Lang,
) -> AppResult<bool> {
    let encrypted_name: Option<String> = if let Some(name) = section_name {
        let user_key: String = get_user_encryption_key(user_id)?;
        Some(encrypt_data_with_user_key(name, &user_key)?)
    } else {
        None
    };
    let original_name_hash: Option<String> = section_name.map(hash_element);
    let last_update: i64 = timestamp_in_seconds();
    repo::update_section_with_series_link(conn, user_id, section_id, encrypted_name.as_deref(), original_name_hash.as_deref(), series_location_id, last_update, lang)
}

/// Deletes a location section and all its associated elements and sub-elements.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `book_id` - The book's unique identifier
/// * `location_id` - The location's unique identifier to delete
/// * `deleted_at` - The timestamp of deletion
/// * `lang` - The language for error messages
/// Returns true if the deletion was successful.
pub fn delete_location_section(conn: &Connection, user_id: &str, book_id: &str, location_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
    let deleted: bool = repo::delete_location_section(conn, user_id, location_id, lang)?;
    if deleted {
        tombstone_repo::insert(conn, location_id, "book_location", location_id, Some(book_id), user_id, deleted_at, lang)?;
    }
    Ok(deleted)
}

/// Deletes a location element and all its associated sub-elements.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `book_id` - The book's unique identifier
/// * `element_id` - The element's unique identifier to delete
/// * `deleted_at` - The timestamp of deletion
/// * `lang` - The language for error messages
/// Returns true if the deletion was successful.
pub fn delete_location_element(conn: &Connection, user_id: &str, book_id: &str, element_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
    let deleted: bool = repo::delete_location_element(conn, user_id, element_id, lang)?;
    if deleted {
        tombstone_repo::insert(conn, element_id, "location_element", element_id, Some(book_id), user_id, deleted_at, lang)?;
    }
    Ok(deleted)
}

/// Deletes a location sub-element.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `book_id` - The book's unique identifier
/// * `sub_element_id` - The sub-element's unique identifier to delete
/// * `deleted_at` - The timestamp of deletion
/// * `lang` - The language for error messages
/// Returns true if the deletion was successful.
pub fn delete_location_sub_element(conn: &Connection, user_id: &str, book_id: &str, sub_element_id: &str, deleted_at: i64, lang: Lang) -> AppResult<bool> {
    let deleted: bool = repo::delete_location_sub_element(conn, user_id, sub_element_id, lang)?;
    if deleted {
        tombstone_repo::insert(conn, sub_element_id, "location_sub_element", sub_element_id, Some(book_id), user_id, deleted_at, lang)?;
    }
    Ok(deleted)
}

/// Retrieves location tags (elements or sub-elements) for tagging purposes.
/// Returns sub-elements when an element has multiple sub-elements, otherwise returns the element itself.
/// * `conn` - Database connection
/// * `user_id` - The user's unique identifier
/// * `book_id` - The book's unique identifier
/// * `lang` - The language for error messages
/// Returns an array of sub-elements suitable for tagging.
pub fn get_location_tags(conn: &Connection, user_id: &str, book_id: &str, lang: Lang) -> AppResult<Vec<SubElement>> {
    let tag_records: Vec<repo::LocationElementQueryResult> = repo::fetch_location_tags(conn, user_id, book_id, lang)?;
    if tag_records.is_empty() {
        return Ok(vec![]);
    }

    let user_key: String = get_user_encryption_key(user_id)?;

    let mut element_counts: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
    for record in &tag_records {
        *element_counts.entry(record.element_id.clone()).or_insert(0) += 1;
    }

    let mut sub_elements: Vec<SubElement> = Vec::new();
    let mut processed_ids: std::collections::HashSet<String> = std::collections::HashSet::new();

    for record in &tag_records {
        let element_count: usize = *element_counts.get(&record.element_id).unwrap_or(&0);

        if element_count > 1 {
            if let Some(ref sub_element_id) = record.sub_element_id {
                if processed_ids.contains(sub_element_id) {
                    continue;
                }
                let decrypted_name: String = match record.sub_elem_name.as_deref() {
                    Some(name) if !name.is_empty() => decrypt_data_with_user_key(name, &user_key)?,
                    _ => String::new(),
                };
                let decrypted_description: String = match record.sub_elem_description.as_deref() {
                    Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
                    _ => String::new(),
                };
                sub_elements.push(SubElement {
                    id: sub_element_id.clone(),
                    name: decrypted_name,
                    description: decrypted_description,
                });
                processed_ids.insert(sub_element_id.clone());
            }
        } else if element_count == 1 && record.sub_element_id.is_none() {
            if processed_ids.contains(&record.element_id) {
                continue;
            }
            let decrypted_name: String = decrypt_data_with_user_key(&record.element_name, &user_key)?;
            let decrypted_description: String = match record.element_description.as_deref() {
                Some(description) if !description.is_empty() => decrypt_data_with_user_key(description, &user_key)?,
                _ => String::new(),
            };
            sub_elements.push(SubElement {
                id: record.element_id.clone(),
                name: decrypted_name,
                description: decrypted_description,
            });
            processed_ids.insert(record.element_id.clone());
        }
    }

    Ok(sub_elements)
}

