use std::collections::HashMap;

use super::{common::PaginatedResponse, issuance::get_issuance_index};
use crate::{
    accounts::NftIssuanceVault, find_bond_pda, sdk::common::DiscriminatorKey, types::Discriminator,
    STABLEBOND_ID,
};
use anyhow::{anyhow, Result};
use borsh::BorshDeserialize;
use solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig};
use solana_client::{
    nonblocking::rpc_client::RpcClient,
    rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
    rpc_filter::{Memcmp, RpcFilterType},
};
use solana_sdk::{account::ReadableAccount, commitment_config::CommitmentConfig, pubkey::Pubkey};

pub async fn get_nft_issuance_vaults(
    rpc_client: &RpcClient,
    page: usize,
    size: usize,
) -> Result<PaginatedResponse<NftIssuanceVault>> {
    get_nft_issuance_vaults_with_filter(rpc_client, None, page, size).await
}

pub async fn get_nft_issuance_vaults_by_issuance(
    rpc_client: &RpcClient,
    issuance: Pubkey,
    page: usize,
    size: usize,
) -> Result<PaginatedResponse<NftIssuanceVault>> {
    get_nft_issuance_vaults_with_filter(rpc_client, Some(issuance), page, size).await
}

pub async fn get_nft_issuance_vaults_by_mint(
    rpc_client: &RpcClient,
    mint: Pubkey,
    page: usize,
    size: usize,
) -> Result<PaginatedResponse<NftIssuanceVault>> {
    if size > 100 {
        return Err(anyhow!("Size must be less than or equal to 100"));
    }

    let issuance_addresses = get_issuance_addresses_by_mint(rpc_client, mint).await?;

    let mut nft_issuance_vaults = Vec::<(String, NftIssuanceVault)>::new();
    for issuance_address in issuance_addresses {
        // TODO: Paging here is busted because it is 2 levels deep. This is an expensive query.
        // For now, it does the best it can, but this won't work right as volume increases (missing redemptions).
        match get_nft_issuance_vaults_by_issuance(rpc_client, issuance_address, 1, 100).await {
            Ok(response) => {
                response
                    .items
                    .into_iter()
                    .for_each(|(address, item)| nft_issuance_vaults.push((address, item)));
            }
            Err(_) => continue,
        }
    }

    let total_items = nft_issuance_vaults.len();
    let start_index = (page - 1) * size;
    if start_index > total_items {
        return Ok(PaginatedResponse {
            items: vec![],
            page,
            size: 0,
            total_items,
        });
    }

    let mut end_index = page * size;
    if end_index > total_items {
        end_index = total_items;
    }

    let nft_issuance_vaults = &nft_issuance_vaults[start_index..end_index];
    Ok(PaginatedResponse {
        items: nft_issuance_vaults.to_vec(),
        page,
        size,
        total_items,
    })
}

async fn get_nft_issuance_vaults_with_filter(
    rpc_client: &RpcClient,
    issuance: Option<Pubkey>,
    page: usize,
    size: usize,
) -> Result<PaginatedResponse<NftIssuanceVault>> {
    if size > 100 {
        return Err(anyhow!("Size must be less than or equal to 100"));
    }

    let nft_issuance_vault_index = get_nft_issuance_vault_index(rpc_client, issuance).await?;
    let total_items = nft_issuance_vault_index.len();

    let start_index = (page - 1) * size;
    if start_index > total_items {
        return Ok(PaginatedResponse {
            items: vec![],
            page,
            size: 0,
            total_items,
        });
    }

    let mut end_index = page * size;
    if end_index > total_items {
        end_index = total_items;
    }

    // take a slice of the nft_issuance_vault_index
    let nft_issuance_vault_addresses = nft_issuance_vault_index
        .into_iter()
        .map(|(address, _)| address)
        .collect::<Vec<Pubkey>>();

    let nft_issuance_vault_addresses = &nft_issuance_vault_addresses[start_index..end_index];
    let response =
        get_nft_issuance_vaults_by_addresses(rpc_client, nft_issuance_vault_addresses).await?;

    Ok(PaginatedResponse {
        items: response,
        page,
        size,
        total_items,
    })
}

async fn get_nft_issuance_vault_index(
    rpc_client: &RpcClient,
    issuance: Option<Pubkey>,
) -> Result<Vec<(Pubkey, Pubkey)>> {
    let nft_issuance_vault_discriminator_key: DiscriminatorKey =
        Discriminator::NftIssuanceVault.into(); // Get nft issuance vaults

    let mut filter_vec = vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
        0,
        &[nft_issuance_vault_discriminator_key.into()],
    ))];

    if issuance.is_some() {
        filter_vec.push(RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
            33,
            &issuance.unwrap().to_bytes(),
        )));
    }

    let config = RpcProgramAccountsConfig {
        account_config: RpcAccountInfoConfig {
            commitment: Some(CommitmentConfig::finalized()),
            encoding: Some(UiAccountEncoding::Base64),
            data_slice: Some(UiDataSliceConfig {
                // Grab the owner address from the account
                offset: 1,
                length: 32,
            }),
            min_context_slot: None,
        },
        filters: Some(filter_vec),
        with_context: Some(true),
    };

    let nft_issuance_vault_index = rpc_client
        .get_program_accounts_with_config(&STABLEBOND_ID, config)
        .await?;

    let mut nft_issuance_vaults_with_authority = Vec::<(Pubkey, Pubkey)>::new();
    for stuff in nft_issuance_vault_index.into_iter() {
        let (address, account) = stuff;
        if let Ok(owner) = Pubkey::try_from_slice(&account.data()) {
            nft_issuance_vaults_with_authority.push((address, owner));
        }
    }
    nft_issuance_vaults_with_authority.sort_by(|a, b| a.1.cmp(&b.1));

    Ok(nft_issuance_vaults_with_authority)
}

async fn get_nft_issuance_vaults_by_addresses(
    rpc_client: &RpcClient,
    nft_issuance_vault_addresses: &[Pubkey],
) -> Result<Vec<(String, NftIssuanceVault)>> {
    let nft_issuance_vaults = rpc_client
        .get_multiple_accounts(&nft_issuance_vault_addresses)
        .await?;

    let mut nft_issuance_vault_map = HashMap::<Pubkey, NftIssuanceVault>::new();
    for (index, option_account) in nft_issuance_vaults.into_iter().enumerate() {
        if let Some(nft_issuance_vault_account) = option_account {
            let account = NftIssuanceVault::from_bytes(nft_issuance_vault_account.data())?;
            nft_issuance_vault_map.insert(nft_issuance_vault_addresses[index], account);
        }
    }

    let mut response = Vec::<(String, NftIssuanceVault)>::new();
    for addr in nft_issuance_vault_addresses.into_iter() {
        if let Some(val) = nft_issuance_vault_map.get(addr) {
            response.push((addr.to_string(), val.clone()));
        }
    }

    Ok(response)
}

async fn get_issuance_addresses_by_mint(
    rpc_client: &RpcClient,
    mint: Pubkey,
) -> Result<Vec<Pubkey>> {
    let (bond_address, _) = find_bond_pda(mint);
    let issuance_index = get_issuance_index(rpc_client, bond_address).await?;
    let issuance_addresses = issuance_index
        .into_iter()
        .map(|(address, _)| address)
        .collect();
    Ok(issuance_addresses)
}
