use crate::cache::{get_hash, get_str};
use crate::common::{
    deserialize_from_json, get_hash_item, get_items, insert_items, serialize_to_json, ACCOUNTS_KEY,
    ACCOUNT_IDENT_KEY,
};
use cal_core::accounting::Address;
use cal_core::device::device::DeviceStruct;
use cal_core::{Account, AccountLite, Asset, Hook, Trunk, DDI};
use redis::aio::MultiplexedConnection;
use redis::{AsyncCommands, RedisError, RedisResult, Value};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::Display;

/// Retrieves all accounts stored in Redis and deserializes them into `AccountLite` structs.
///
/// This function uses the common `get_items` helper which handles fetching and deserialization.
///
/// # Arguments
/// * `con` - Redis connection to use for fetching data
///
/// # Returns
/// * `Result<Vec<AccountLite>, RedisError>` - List of account summaries or an error
pub async fn get_accounts(con: MultiplexedConnection) -> Result<Vec<AccountLite>, RedisError> {
    get_items(con, ACCOUNTS_KEY).await
}

/// Creates a Redis key for an account's collection
///
/// Formats a consistent Redis key pattern for account-related collections.
///
/// # Arguments
/// * `account_id` - Unique identifier for the account
/// * `collection` - Name of the collection (e.g., "devices", "ddis")
///
/// # Returns
/// * `String` - Formatted Redis key in the pattern "cal:account:{id}:{collection}"
fn account_collection_key(account_id: &str, collection: &str) -> String {
    format!("cal:account:{}:{}", account_id, collection)
}

/// Retrieves an account by its identifier (e.g., username, email).
///
/// First looks up the account ID from the identifiers hash, then retrieves
/// the full account details using that ID.
///
/// # Arguments
/// * `con` - Redis connection
/// * `key` - Account identifier to look up (username, email, etc.)
///
/// # Returns
/// * `Result<Option<AccountLite>, RedisError>` - Account if found, None if not found, or a Redis error
pub async fn get_account_by_ident(
    con: MultiplexedConnection,
    key: &str,
) -> Result<Option<AccountLite>, RedisError> {
    // First try to get the account ID
    match get_hash(con.clone(), ACCOUNT_IDENT_KEY, key).await? {
        Some(account_id) => get_account_by_id(con, &account_id).await,
        None => Ok(None), // No account found with this identifier
    }
}

/// Retrieves an account by its unique ID.
///
/// # Arguments
/// * `con` - Redis connection
/// * `key` - Account ID
///
/// # Returns
/// * `Result<Option<AccountLite>, RedisError>` - Account if found, None if not found, or a Redis error
pub async fn get_account_by_id(
    con: MultiplexedConnection,
    key: &str,
) -> Result<Option<AccountLite>, RedisError> {
    let account_key = format!("cal:account:{}", key);

    match get_str(con, &account_key).await? {
        Some(value) => Ok(Some(deserialize_from_json(&value)?)),
        None => Ok(None),
    }
}

/// Inserts a complete account with all its related data into Redis.
///
/// This function:
/// 1. Stores the basic account information
/// 2. Builds account identity mappings for lookups
/// 3. Stores all related collections (devices, DDIs, trunks, hooks, assets, addresses)
///
/// # Arguments
/// * `con` - Redis connection
/// * `account` - The complete account to insert with all related data
///
/// # Returns
/// * `Result<(), RedisError>` - Success indicator or an error
pub async fn insert_account(
    mut con: MultiplexedConnection,
    account: Account,
) -> Result<(), RedisError> {
    // Store account lite info
    let account_lite: AccountLite = account.clone().into();
    let value = serialize_to_json(&account_lite)?;

    // Explicitly specify the return type as Value
    let _: Value = con.hset(ACCOUNTS_KEY, &account.id, value).await?;

    // Build account identity mappings
    build_account_idents(&mut con, &account).await?;

    // Insert all the related collections with explicit type annotations
    let _: Value = insert_collection(con.clone(), &account.id, "ddis", &account.ddis, |ddi| {
        ddi.id.clone()
    })
    .await?;
    let _: Value = insert_collection(
        con.clone(),
        &account.id,
        "devices",
        &account.devices,
        |device| device.id.clone(),
    )
    .await?;
    let _: Value = insert_collection(
        con.clone(),
        &account.id,
        "trunks",
        &account.trunks,
        |trunk| trunk.id.clone(),
    )
    .await?;
    let _: Value = insert_collection(con.clone(), &account.id, "hooks", &account.hooks, |hook| {
        hook.id.clone()
    })
    .await?;

    let _: Value = insert_collection(
        con.clone(),
        &account.id,
        "assets",
        &account.assets,
        |asset| asset.id.clone(),
    )
    .await?;

    // Special handling for addresses
    let _: Value = insert_addresses(
        con.clone(),
        &account.id,
        account.address.clone(),
        &account.addresses,
    )
    .await?;

    let _: Value =
        insert_device_idents(con.clone(), &account.id, &account.devices, &account.ddis).await?;

    Ok(())
}

/// Generic function to insert items into an account-specific collection
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - ID of the account owning the collection
/// * `collection_name` - Name of the collection (e.g., "devices", "ddis")
/// * `items` - The items to insert
/// * `id_extractor` - Function to extract the unique ID from each item
///
/// # Returns
/// * `RedisResult<Value>` - Redis operation result
///
/// # Type Parameters
/// * `T` - Type of items being inserted, must implement Serialize
/// * `F` - Type of ID extractor function
async fn insert_collection<T, F>(
    con: MultiplexedConnection,
    account_id: &str,
    collection_name: &str,
    items: &[T],
    id_extractor: F,
) -> RedisResult<Value>
where
    T: Serialize,
    F: Fn(&T) -> String,
{
    let key = account_collection_key(account_id, collection_name);
    insert_items(con, &key, items, id_extractor).await
}

/// Generic function to get all items from an account-specific collection
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - ID of the account owning the collection
/// * `collection_name` - Name of the collection (e.g., "devices", "ddis")
///
/// # Returns
/// * `Result<Vec<T>, RedisError>` - List of deserialized items or an error
///
/// # Type Parameters
/// * `T` - Type to deserialize items into, must implement DeserializeOwned
async fn get_collection<T: DeserializeOwned>(
    con: MultiplexedConnection,
    account_id: &str,
    collection_name: &str,
) -> Result<Vec<T>, RedisError> {
    let key = account_collection_key(account_id, collection_name);
    get_items(con, &key).await
}

/// Generic function to get a specific item from an account-specific collection
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - ID of the account owning the collection
/// * `collection_name` - Name of the collection (e.g., "devices", "ddis")
/// * `item_id` - ID of the specific item to retrieve
///
/// # Returns
/// * `Result<Option<T>, RedisError>` - The item if found, None if not found, or an error
///
/// # Type Parameters
/// * `T` - Type to deserialize the item into, must implement DeserializeOwned
/// * `I` - Type of the item ID, must implement Display
async fn get_collection_item<T: DeserializeOwned, I: Display>(
    con: MultiplexedConnection,
    account_id: &str,
    collection_name: &str,
    item_id: I,
) -> Result<Option<T>, RedisError> {
    let key = account_collection_key(account_id, collection_name);
    get_hash_item(con, &key, &item_id.to_string()).await
}

/// Builds Redis identity mappings for an account
///
/// Creates mappings between various identifiers (account ID, domain, DDI names,
/// trunk IPs, device usernames) and the account ID. These mappings are used for
/// quick lookups by identifiers.
///
/// # Arguments
/// * `con` - Mutable Redis connection
/// * `account` - The account containing identifiers to map
///
/// # Returns
/// * `Result<(), RedisError>` - Success indicator or an error
async fn build_account_idents(
    con: &mut MultiplexedConnection,
    account: &Account,
) -> Result<(), RedisError> {
    // Add account ID and domain mappings
    let mut ident_entries = vec![
        (account.id.clone(), account.id.clone()),
        (account.domain.clone(), account.id.clone()),
    ];

    // Add DDI name mappings
    for ddi in &account.ddis {
        ident_entries.push((ddi.name.clone(), account.id.clone()));

        // Add trunk-specific DDI mappings
        for trunk in &account.trunks {
            let key = format!("cal:trunk:{}", trunk.ip);
            let _: Value = con.hset(&key, &ddi.name, &account.id).await?;
        }
    }

    // Add device client username mappings
    for device in &account.devices {
        if let Some(client) = &device.client {
            let user_domain = format!("{}@connect.callable.io", client.username);
            ident_entries.push((user_domain, account.id.clone()));
        }
    }

    // Insert all identity entries
    let _: Value = con.hset_multiple(ACCOUNT_IDENT_KEY, &ident_entries).await?;

    Ok(())
}

//
// DEVICE RELATED FUNCTIONS
//

/// Retrieves all devices for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to get devices for
///
/// # Returns
/// * `Result<Vec<DeviceStruct>, RedisError>` - List of devices or an error
pub async fn get_devices(
    con: MultiplexedConnection,
    account_id: &str,
) -> Result<Vec<DeviceStruct>, RedisError> {
    get_collection(con, account_id, "devices").await
}

/// Retrieves a specific device for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID owning the device
/// * `device_id` - Device ID to retrieve
///
/// # Returns
/// * `Result<Option<DeviceStruct>, RedisError>` - The device if found, None if not found, or an error
pub async fn get_device(
    con: MultiplexedConnection,
    account_id: &str,
    device_id: &str,
) -> Result<Option<DeviceStruct>, RedisError> {
    get_collection_item(con, account_id, "devices", device_id).await
}

/// Inserts multiple devices for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to add devices to
/// * `devices` - List of devices to insert
///
/// # Returns
/// * `RedisResult<Value>` - Redis operation result
pub async fn insert_devices(
    con: MultiplexedConnection,
    account_id: &str,
    devices: &[DeviceStruct],
) -> RedisResult<Value> {
    insert_collection(con, account_id, "devices", devices, |device| {
        device.id.clone()
    })
    .await
}

//
// DDI RELATED FUNCTIONS
//

/// Retrieves all DDIs for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to get DDIs for
///
/// # Returns
/// * `Result<Vec<DDI>, RedisError>` - List of DDIs or an error
pub async fn get_ddis(
    con: MultiplexedConnection,
    account_id: &str,
) -> Result<Vec<DDI>, RedisError> {
    get_collection(con, account_id, "ddis").await
}

/// Retrieves a specific DDI for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID owning the DDI
/// * `ddi_id` - DDI ID to retrieve
///
/// # Returns
/// * `Result<Option<DDI>, RedisError>` - The DDI if found, None if not found, or an error
pub async fn get_ddi(
    con: MultiplexedConnection,
    account_id: &str,
    ddi_id: &str,
) -> Result<Option<DDI>, RedisError> {
    get_collection_item(con, account_id, "ddis", ddi_id).await
}

pub async fn get_device_by_ident(
    con: MultiplexedConnection,
    account_id: &str,
    id: &str,
) -> Result<Option<DeviceStruct>, RedisError> {
    get_collection_item(con, account_id, "device_idents", id).await
}

pub async fn get_outbound_device(
    con: MultiplexedConnection,
    account_id: &str,
) -> Result<Option<DeviceStruct>, RedisError> {
    get_hash_item(con, account_id, "outbound_route").await
}

/// Inserts multiple DDIs for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to add DDIs to
/// * `ddis` - List of DDIs to insert
///
/// # Returns
/// * `RedisResult<Value>` - Redis operation result
pub async fn insert_ddis(
    con: MultiplexedConnection,
    account_id: &str,
    ddis: &[DDI],
) -> RedisResult<Value> {
    insert_collection(con, account_id, "ddis", ddis, |ddi| ddi.id.clone()).await
}

//
// HOOK RELATED FUNCTIONS
//

/// Retrieves all hooks for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to get hooks for
///
/// # Returns
/// * `Result<Vec<Hook>, RedisError>` - List of hooks or an error
pub async fn get_hooks(
    con: MultiplexedConnection,
    account_id: &str,
) -> Result<Vec<Hook>, RedisError> {
    get_collection(con, account_id, "hooks").await
}

/// Retrieves a specific hook for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID owning the hook
/// * `hook_id` - Hook ID to retrieve
///
/// # Returns
/// * `Result<Option<Hook>, RedisError>` - The hook if found, None if not found, or an error
pub async fn get_hook(
    con: MultiplexedConnection,
    account_id: &str,
    hook_id: &str,
) -> Result<Option<Hook>, RedisError> {
    get_collection_item(con, account_id, "hooks", hook_id).await
}

/// Inserts multiple hooks for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to add hooks to
/// * `hooks` - List of hooks to insert
///
/// # Returns
/// * `RedisResult<Value>` - Redis operation result
pub async fn insert_hooks(
    con: MultiplexedConnection,
    account_id: &str,
    hooks: &[Hook],
) -> RedisResult<Value> {
    insert_collection(con, account_id, "hooks", hooks, |hook| hook.id.clone()).await
}

//
// TRUNK RELATED FUNCTIONS
//

/// Retrieves all trunks for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to get trunks for
///
/// # Returns
/// * `Result<Vec<Trunk>, RedisError>` - List of trunks or an error
pub async fn get_trunks(
    con: MultiplexedConnection,
    account_id: &str,
) -> Result<Vec<Trunk>, RedisError> {
    get_collection(con, account_id, "trunks").await
}

/// Retrieves a specific trunk for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID owning the trunk
/// * `trunk_id` - Trunk ID to retrieve
///
/// # Returns
/// * `Result<Option<Trunk>, RedisError>` - The trunk if found, None if not found, or an error
pub async fn get_trunk(
    con: MultiplexedConnection,
    account_id: &str,
    trunk_id: &str,
) -> Result<Option<Trunk>, RedisError> {
    get_collection_item(con, account_id, "trunks", trunk_id).await
}

/// Looks up an account by DDI ID and trunk IP.
///
/// This is used for routing calls from trunks to the correct account
/// by matching the incoming DDI and the trunk's IP address.
///
/// # Arguments
/// * `con` - Redis connection
/// * `ddi_id` - DDI identifier (typically the phone number)
/// * `trunk_ip` - IP address of the trunk
///
/// # Returns
/// * `Result<Option<AccountLite>, RedisError>` - The account if found, None if not found, or an error
pub async fn get_trunk_and_ddi(
    con: MultiplexedConnection,
    ddi_id: &str,
    trunk_ip: &str,
) -> Result<Option<AccountLite>, RedisError> {
    let key = format!("cal:trunk:{}", trunk_ip);

    // Get the account ID associated with this DDI and trunk
    let id: Option<String> = con.clone().hget(&key, ddi_id).await?;

    match id {
        Some(account_id) => get_account_by_ident(con, &account_id).await,
        None => Ok(None),
    }
}

/// Inserts multiple trunks for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to add trunks to
/// * `trunks` - List of trunks to insert
///
/// # Returns
/// * `RedisResult<Value>` - Redis operation result
pub async fn insert_trunks(
    con: MultiplexedConnection,
    account_id: &str,
    trunks: &[Trunk],
) -> RedisResult<Value> {
    insert_collection(con, account_id, "trunks", trunks, |trunk| trunk.id.clone()).await
}

//
// ASSET RELATED FUNCTIONS
//

/// Retrieves all assets for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to get assets for
///
/// # Returns
/// * `Result<Vec<Asset>, RedisError>` - List of assets or an error
pub async fn get_assets(
    con: MultiplexedConnection,
    account_id: &str,
) -> Result<Vec<Asset>, RedisError> {
    get_collection(con, account_id, "assets").await
}

/// Retrieves a specific asset for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID owning the asset
/// * `asset_id` - Asset ID to retrieve
///
/// # Returns
/// * `Result<Option<Asset>, RedisError>` - The asset if found, None if not found, or an error
pub async fn get_asset(
    con: MultiplexedConnection,
    account_id: &str,
    asset_id: &str,
) -> Result<Option<Asset>, RedisError> {
    get_collection_item(con, account_id, "assets", asset_id).await
}

/// Inserts multiple assets for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to add assets to
/// * `assets` - List of assets to insert
///
/// # Returns
/// * `RedisResult<Value>` - Redis operation result
pub async fn insert_assets(
    con: MultiplexedConnection,
    account_id: &str,
    assets: &[Asset],
) -> RedisResult<Value> {
    insert_collection(con, account_id, "assets", assets, |asset| asset.id.clone()).await
}

//
// ADDRESS RELATED FUNCTIONS
//

/// Retrieves all addresses for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to get addresses for
///
/// # Returns
/// * `Result<Vec<Address>, RedisError>` - List of addresses or an error
pub async fn get_addresses(
    con: MultiplexedConnection,
    account_id: &str,
) -> Result<Vec<Address>, RedisError> {
    get_collection(con, account_id, "addresses").await
}

/// Retrieves a specific address for an account.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID owning the address
/// * `address_id` - Address ID to retrieve
///
/// # Returns
/// * `Result<Option<Address>, RedisError>` - The address if found, None if not found, or an error
pub async fn get_address(
    con: MultiplexedConnection,
    account_id: &str,
    address_id: &str,
) -> Result<Option<Address>, RedisError> {
    get_collection_item(con, account_id, "addresses", address_id).await
}

/// Inserts addresses for an account, with special handling for the default address.
///
/// # Arguments
/// * `con` - Redis connection
/// * `account_id` - Account ID to add addresses to
/// * `default_address` - The default address (stored with key "default")
/// * `additional_addresses` - Additional addresses to insert (stored with their IDs)
///
/// # Returns
/// * `RedisResult<Value>` - Redis operation result
async fn insert_addresses(
    mut con: MultiplexedConnection,
    account_id: &str,
    default_address: Address,
    additional_addresses: &[Address],
) -> RedisResult<Value> {
    let key = account_collection_key(account_id, "addresses");
    let mut entries = Vec::with_capacity(additional_addresses.len() + 1);

    // Add default address
    entries.push(("default".to_string(), serialize_to_json(&default_address)?));

    // Add additional addresses
    for address in additional_addresses {
        entries.push((address.id.clone(), serialize_to_json(address)?));
    }

    con.hset_multiple(key, &entries).await
}

async fn insert_device_idents(
    mut con: MultiplexedConnection,
    account_id: &str,
    devices: &[DeviceStruct],
    ddis: &[DDI],
) -> RedisResult<Value> {
    let key = account_collection_key(account_id, "device_idents");
    let _: Value = con.del(key.clone()).await?;

    let mut entries: Vec<(String, String)> = Vec::new();

    for device in devices {
        if (device.clone().start_route.is_some()) {
            for ddi in ddis {
                if (device.clone().start_route.unwrap().ddis.contains(&ddi.id)) {
                    entries.push((ddi.name.clone(), device.id.clone()));
                }
            }
        }
    }

    for device in devices {
        if device.clone().regex_route.is_some() {
            entries.push(("OUTBOUND_ROUTE".to_string(), device.id.clone()));
        }
    }

    con.hset_multiple(key, &entries).await
}

/// Inserts account identifiers into a global lookup hash.
///
/// This allows looking up accounts by alternative identifiers like emails,
/// usernames, or domains.
///
/// # Arguments
/// * `con` - Redis connection
/// * `items` - Pairs of (identifier, account_id) to insert
///
/// # Returns
/// * `RedisResult<Value>` - Redis operation result
pub async fn insert_account_idents(
    mut con: MultiplexedConnection,
    items: &[(String, String)],
) -> RedisResult<Value> {
    con.hset_multiple(ACCOUNT_IDENT_KEY, items).await
}
