use super::common::{remove_null_bytes, DiscriminatorKey};
use crate::{
    accounts::{
        AccessPass, BondMultisigMeta, Config, Delegate, Issuance, Kyc, NftIssuanceVault, Offramp,
        Onramp, Payment, PaymentFeed, Payout, PurchaseOrder, SellLiquidity,
    },
    find_issuance_pda, generated,
    types::Discriminator,
    STABLEBOND_ID,
};

use ::{
    anyhow::{anyhow, Result},
    mpl_token_metadata::accounts::Metadata,
    solana_client::nonblocking::rpc_client::RpcClient,
    solana_program::pubkey::Pubkey,
    solana_sdk::account::ReadableAccount,
    spl_token_2022::{
        extension::{
            interest_bearing_mint::InterestBearingConfig as InterestBearingConfigExt,
            metadata_pointer::MetadataPointer as MetadataPointerExt, BaseStateWithExtensions,
            StateWithExtensions,
        },
        state::Mint,
    },
    std::str::FromStr,
};

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct IdentifiedAccount<T> {
    pub id: String,
    pub account: T,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MetadataPointer {
    pub authority: Option<String>,
    pub metadata_address: String,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct InterestBearingConfig {
    pub rate_authority: String,
    pub initialization_timestamp: i64,
    pub pre_update_average_rate: i16,
    pub last_update_timestamp: i64,
    pub current_rate: i16,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BondMetadata {
    pub metadata_pointer: MetadataPointer,
    pub metadata: Metadata,
    pub interest_bearing_config: InterestBearingConfig,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BondWithMetadata {
    pub bond: generated::accounts::Bond,
    pub bond_metadata: BondMetadata,
}

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct GetProgramAccountsOutput {
    pub configs: Vec<IdentifiedAccount<Config>>,
    pub delegates: Vec<IdentifiedAccount<Delegate>>,
    pub bonds: Vec<IdentifiedAccount<BondWithMetadata>>,
    pub issuances: Vec<IdentifiedAccount<Issuance>>,
    pub payments: Vec<IdentifiedAccount<Payment>>,
    pub payouts: Vec<IdentifiedAccount<Payout>>,
    pub nft_issuance_vaults: Vec<IdentifiedAccount<NftIssuanceVault>>,
    pub payment_feeds: Vec<IdentifiedAccount<PaymentFeed>>,
    pub access_passes: Vec<IdentifiedAccount<AccessPass>>,
    pub kycs: Vec<IdentifiedAccount<Kyc>>,
    pub sell_liquidity: Vec<IdentifiedAccount<SellLiquidity>>,
    pub offramp: Vec<IdentifiedAccount<Offramp>>,
    pub onramp: Vec<IdentifiedAccount<Onramp>>,
    pub bond_multisig_metas: Vec<IdentifiedAccount<BondMultisigMeta>>,
}

async fn get_bond_metadata(mint: Pubkey, client: &RpcClient) -> Result<BondMetadata> {
    let mint_account = client.get_account(&mint).await?;
    let state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;
    let metadata_ext_data = state.get_extension::<MetadataPointerExt>()?;
    let authority = Option::<Pubkey>::from(metadata_ext_data.authority).map(|k| k.to_string());
    let metadata_address = Option::<Pubkey>::from(metadata_ext_data.metadata_address)
        .ok_or_else(|| anyhow!("Metadata key is None"))?;
    let metadata_pointer = MetadataPointer {
        authority,
        metadata_address: metadata_address.to_string(),
    };

    // Alternative way to get the metadata metadata_address
    // However, this way does not allow you to retrieve the
    // metadata pointer data itself, just the address
    // let metadata_address = Metadata::find_pda(&mint).0;

    let interest_bearing_ext_data = state.get_extension::<InterestBearingConfigExt>()?;
    let rate_authority = Option::<Pubkey>::from(interest_bearing_ext_data.rate_authority)
        .map(|k| k.to_string())
        .ok_or_else(|| anyhow!("Rate authority key is None"))?;
    let interest_bearing_config = InterestBearingConfig {
        rate_authority: rate_authority,
        initialization_timestamp: i64::from(interest_bearing_ext_data.initialization_timestamp),
        pre_update_average_rate: i16::from(interest_bearing_ext_data.pre_update_average_rate),
        last_update_timestamp: i64::from(interest_bearing_ext_data.last_update_timestamp),
        current_rate: i16::from(interest_bearing_ext_data.current_rate),
    };

    let metadata_account = client.get_account(&metadata_address).await?;
    let mut metadata = Metadata::from_bytes(&metadata_account.data)?;

    // Remove trailing null bytes
    metadata.name = remove_null_bytes(metadata.name);
    metadata.symbol = remove_null_bytes(metadata.symbol);
    metadata.uri = remove_null_bytes(metadata.uri);

    Ok(BondMetadata {
        metadata_pointer,
        metadata,
        interest_bearing_config,
    })
}

pub async fn get_bond_with_metadata(
    bond_address: Pubkey,
    client: &RpcClient,
) -> Result<BondWithMetadata> {
    let account = client.get_account(&bond_address).await?;
    let bond = generated::accounts::Bond::from_bytes(account.data())?;
    let cloned = bond.clone();
    let metadata = get_bond_metadata(cloned.mint, client).await?;
    Ok(BondWithMetadata {
        bond,
        bond_metadata: metadata,
    })
}

pub async fn get_program_accounts(client: &RpcClient) -> Result<GetProgramAccountsOutput> {
    let accounts = client.get_program_accounts(&STABLEBOND_ID).await?;

    let mut configs = vec![];
    let mut delegates = vec![];
    let mut bonds = vec![];
    let mut issuances = vec![];
    let mut payments = vec![];
    let mut payouts = vec![];
    let mut nft_issuance_vaults = vec![];
    let mut payment_feeds = vec![];
    let mut access_passes = vec![];
    let mut purchase_orders = vec![];
    let mut kycs = vec![];
    let mut sell_liquidity = vec![];
    let mut offramp = vec![];
    let mut onramp = vec![];
    let mut bond_multisig_metas = vec![];

    for (address, account) in accounts {
        if let Ok(account_type) = Discriminator::try_from(DiscriminatorKey::from(account.data()[0]))
        {
            match account_type {
                Discriminator::Bond => {
                    let bond_with_metadata = get_bond_with_metadata(address, &client).await?;
                    bonds.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: bond_with_metadata,
                    });
                }
                Discriminator::Config => {
                    configs.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Config::from_bytes(account.data())?,
                    });
                }
                Discriminator::Delegate => {
                    delegates.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Delegate::from_bytes(account.data())?,
                    });
                }
                Discriminator::Issuance => {
                    issuances.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Issuance::from_bytes(account.data())?,
                    });
                }
                Discriminator::NftIssuanceVault => {
                    nft_issuance_vaults.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: NftIssuanceVault::from_bytes(account.data())?,
                    });
                }
                Discriminator::Payment => {
                    payments.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Payment::from_bytes(account.data())?,
                    });
                }
                Discriminator::Payout => {
                    payouts.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Payout::from_bytes(account.data())?,
                    });
                }
                Discriminator::PaymentFeed => {
                    payment_feeds.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: PaymentFeed::from_bytes(account.data())?,
                    });
                }
                Discriminator::AccessPass => {
                    access_passes.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: AccessPass::from_bytes(account.data())?,
                    });
                }
                Discriminator::PurchaseOrder => {
                    purchase_orders.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: PurchaseOrder::from_bytes(account.data())?,
                    });
                }
                Discriminator::Kyc => {
                    kycs.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Kyc::from_bytes(account.data())?,
                    });
                }
                Discriminator::SellLiquidity => {
                    sell_liquidity.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: SellLiquidity::from_bytes(account.data())?,
                    });
                }
                Discriminator::Offramp => {
                    offramp.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Offramp::from_bytes(account.data())?,
                    });
                }
                Discriminator::Onramp => {
                    onramp.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Onramp::from_bytes(account.data())?,
                    });
                }
                Discriminator::BondMultisigMeta => {
                    bond_multisig_metas.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: BondMultisigMeta::from_bytes(account.data())?,
                    });
                }
            }
        }
    }

    let bond_issuances: Vec<(String, u64)> = bonds
        .clone()
        .into_iter()
        .map(|b| (b.id, b.account.bond.issuance_number + 1)) // Account for upcoming issuance
        .collect();

    let mut items: Vec<(String, u64, String)> = vec![];
    for (bond_address, issuance_number) in bond_issuances {
        let bond_address_str = Pubkey::from_str(bond_address.trim())?;
        let mut issuance_number = issuance_number;
        while issuance_number > 0 {
            let (issuance_address, _) =
                find_issuance_pda(bond_address_str.clone(), issuance_number);
            items.push((
                bond_address.clone(),
                issuance_number,
                issuance_address.to_string(),
            ));
            issuance_number = issuance_number - 1;
        }
    }

    items.sort_by(|a, b| a.0.cmp(&b.0).then(a.1.cmp(&b.1)));

    let mut sorted_issuances: Vec<IdentifiedAccount<Issuance>> = vec![];
    for item in items {
        if let Some(issuance) = issuances.clone().into_iter().find(|i| i.id == item.2) {
            sorted_issuances.push(issuance);
        }
    }

    Ok(GetProgramAccountsOutput {
        configs,
        delegates,
        bonds,
        issuances: sorted_issuances,
        payments,
        payouts,
        nft_issuance_vaults,
        payment_feeds,
        access_passes,
        kycs,
        sell_liquidity,
        offramp,
        onramp,
        bond_multisig_metas,
    })
}
