use super::common::{remove_null_bytes, PaginatedResponse};
use crate::{
    accounts::{Bond, BondMultisigMeta},
    find_bond_multisig_meta_pda, find_bond_pda,
    sdk::common::DiscriminatorKey,
    types::Discriminator,
    STABLEBOND_ID,
};
use anyhow::{anyhow, Result};
use borsh::BorshDeserialize;
use mpl_token_metadata::accounts::Metadata;
use rust_decimal::prelude::*;
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};
use spl_token_2022::{
    extension::{
        interest_bearing_mint::InterestBearingConfig, metadata_pointer::MetadataPointer,
        BaseStateWithExtensions, StateWithExtensions,
    },
    state::Mint,
};
use std::collections::HashMap;
use std::time::{SystemTime, UNIX_EPOCH};

#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct BondDetails {
    pub bond: Bond,
    pub metadata_pointer: MetadataPointerDetails,
    pub metadata: Metadata,
    pub interest_bearing_config: InterestBearingDetails,
    pub current_token_amount: Decimal,
}

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

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

pub async fn get_bond_by_address(rpc_client: &RpcClient, address: Pubkey) -> Result<BondDetails> {
    let bond_account = rpc_client.get_account(&address).await?;
    let account = Bond::from_bytes(bond_account.data())?;

    let mint_account = rpc_client.get_account(&account.mint).await?;
    let state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;
    let metadata_ext_data = state.get_extension::<MetadataPointer>()?;
    let metadata_authority = Option::<Pubkey>::from(metadata_ext_data.authority);
    let metadata_address = Option::<Pubkey>::from(metadata_ext_data.metadata_address)
        .ok_or_else(|| anyhow!("Metadata key is None"))?;
    let interest_cfg = state.get_extension::<InterestBearingConfig>()?;
    let rate_authority = Option::<Pubkey>::from(interest_cfg.rate_authority)
        .map(|k| k.to_string())
        .ok_or_else(|| anyhow!("Rate authority key is None"))?;
    let interest_bearing_config = InterestBearingDetails {
        rate_authority,
        initialization_timestamp: i64::from(interest_cfg.initialization_timestamp),
        pre_update_average_rate: i16::from(interest_cfg.pre_update_average_rate),
        last_update_timestamp: i64::from(interest_cfg.last_update_timestamp),
        current_rate: i16::from(interest_cfg.current_rate),
    };

    let metadata_account = rpc_client.get_account(&metadata_address).await?;
    let mut metadata = Metadata::from_bytes(metadata_account.data())?;
    metadata.name = remove_null_bytes(metadata.name);
    metadata.symbol = remove_null_bytes(metadata.symbol);
    metadata.uri = remove_null_bytes(metadata.uri);

    let current_token_amount = interest_cfg
        .amount_to_ui_amount(
            EXP_10[state.base.decimals as usize],
            state.base.decimals,
            SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() as i64,
        )
        .ok_or_else(|| anyhow!("Failed to calculate current token amount"))?;
    let current_token_amount = Decimal::from_str(&current_token_amount)?;

    Ok(BondDetails {
        bond: account,
        metadata_pointer: MetadataPointerDetails {
            authority: metadata_authority.map(|a| a.to_string()),
            metadata_address: metadata_address.to_string(),
        },
        metadata,
        interest_bearing_config,
        current_token_amount,
    })
}

pub async fn get_bond_by_mint(
    rpc_client: &RpcClient,
    mint: Pubkey,
) -> Result<(String, BondDetails)> {
    let bond_index = get_bond_index(rpc_client).await?;

    let bond_address = bond_index
        .into_iter()
        .find(|(_, mint_address)| *mint_address == mint)
        .map(|(address, _)| address);

    match bond_address {
        Some(bond_address) => get_bond_by_address(rpc_client, bond_address)
            .await
            .map(|details| (bond_address.to_string(), details)),
        None => Err(anyhow!("No bond found for mint")),
    }
}

pub async fn get_bond_mint_auth_multisig_if_needed(
    rpc_client: &RpcClient,
    mint_account: Pubkey,
) -> Result<Option<Pubkey>> {
    let bond_account = find_bond_pda(mint_account).0;
    let data = rpc_client.get_account_data(&bond_account).await?;
    let bond = Bond::from_bytes(&data).unwrap();

    // Get the mint_auth_multisig_account if is_authority_multisig is true
    let mint_auth_multisig_account = if bond.is_authority_multisig {
        let bond_multisig_meta_account = find_bond_multisig_meta_pda(mint_account).0;
        let bond_multisig_meta_data = rpc_client
            .get_account_data(&bond_multisig_meta_account)
            .await?;
        let bond_multisig_meta = BondMultisigMeta::from_bytes(&bond_multisig_meta_data).unwrap();
        Some(bond_multisig_meta.mint_auth_multisig)
    } else {
        None
    };

    Ok(mint_auth_multisig_account)
}

async fn get_bond_index(rpc_client: &RpcClient) -> Result<Vec<(Pubkey, Pubkey)>> {
    let bond_discriminator_key: DiscriminatorKey = Discriminator::Bond.into(); // Get bonds
    let filter_vec = vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
        0,
        &[bond_discriminator_key.into()],
    ))];

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

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

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

    Ok(bonds_with_mints)
}

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

    let bond_index = get_bond_index(rpc_client).await?;
    let total_items = bond_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 bond_index
    let bond_addresses = bond_index
        .into_iter()
        .map(|(address, _)| address)
        .collect::<Vec<Pubkey>>();

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

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

async fn get_bonds_by_addresses(
    rpc_client: &RpcClient,
    bond_addresses: &[Pubkey],
) -> Result<Vec<(String, BondDetails)>> {
    let bonds = rpc_client.get_multiple_accounts(&bond_addresses).await?;

    let mut bond_to_mint = HashMap::<Pubkey, Pubkey>::new();
    let mut bond_map = HashMap::<Pubkey, Bond>::new();
    for (index, option_account) in bonds.into_iter().enumerate() {
        if let Some(bond_account) = option_account {
            let account = Bond::from_bytes(bond_account.data())?;
            bond_to_mint.insert(bond_addresses[index], account.mint);
            bond_map.insert(bond_addresses[index], account);
        }
    }

    let mint_addresses: Vec<Pubkey> = bond_to_mint.clone().into_values().collect();
    let mints = rpc_client.get_multiple_accounts(&mint_addresses).await?;

    let mut mint_to_metadata = HashMap::<Pubkey, Pubkey>::new();
    let mut metadata_pointer_map = HashMap::<Pubkey, MetadataPointerDetails>::new();
    let mut interest_map = HashMap::<Pubkey, InterestBearingDetails>::new();
    let mut current_token_amount_map = HashMap::<Pubkey, Decimal>::new();
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .unwrap()
        .as_secs() as i64;
    for (index, option_account) in mints.iter().enumerate() {
        if let Some(mint_account) = option_account {
            let state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;
            let metadata_ext_data = state.get_extension::<MetadataPointer>()?;
            let metadata_authority = Option::<Pubkey>::from(metadata_ext_data.authority);
            let metadata_address = Option::<Pubkey>::from(metadata_ext_data.metadata_address)
                .ok_or_else(|| anyhow!("Metadata key is None"))?;
            mint_to_metadata.insert(mint_addresses[index], metadata_address);

            let interest_cfg = state.get_extension::<InterestBearingConfig>()?;

            let current_token_amount = interest_cfg
                .amount_to_ui_amount(
                    EXP_10[state.base.decimals as usize],
                    state.base.decimals,
                    now,
                )
                .ok_or_else(|| anyhow!("Failed to calculate current token amount"))?;
            let current_token_amount = Decimal::from_str(&current_token_amount)?;

            let rate_authority = Option::<Pubkey>::from(interest_cfg.rate_authority)
                .map(|k| k.to_string())
                .ok_or_else(|| anyhow!("Rate authority key is None"))?;
            let interest_bearing_details = InterestBearingDetails {
                rate_authority,
                initialization_timestamp: i64::from(interest_cfg.initialization_timestamp),
                pre_update_average_rate: i16::from(interest_cfg.pre_update_average_rate),
                last_update_timestamp: i64::from(interest_cfg.last_update_timestamp),
                current_rate: i16::from(interest_cfg.current_rate),
            };
            metadata_pointer_map.insert(
                mint_addresses[index],
                MetadataPointerDetails {
                    authority: metadata_authority.map(|a| a.to_string()),
                    metadata_address: metadata_address.to_string(),
                },
            );
            interest_map.insert(mint_addresses[index], interest_bearing_details);
            current_token_amount_map.insert(mint_addresses[index], current_token_amount);
        }
    }

    let metadata_addresses: Vec<Pubkey> = mint_to_metadata.clone().into_values().collect();
    let metadata = rpc_client
        .get_multiple_accounts(&metadata_addresses)
        .await?;

    let mut metadata_map = HashMap::<Pubkey, Metadata>::new();
    for (index, option_account) in metadata.into_iter().enumerate() {
        if let Some(metadata_account) = option_account {
            let mut metadata = Metadata::from_bytes(metadata_account.data())?;
            metadata.name = remove_null_bytes(metadata.name);
            metadata.symbol = remove_null_bytes(metadata.symbol);
            metadata.uri = remove_null_bytes(metadata.uri);
            metadata_map.insert(metadata_addresses[index], metadata);
        }
    }

    let mut bond_details_map = HashMap::<Pubkey, BondDetails>::new();
    for (bond_address, mint_address) in bond_to_mint.into_iter() {
        if let Some(metadata_address) = mint_to_metadata.get(&mint_address) {
            if let (
                Some(bond),
                Some(interest_bearing_details),
                Some(metadata_pointer),
                Some(metadata),
                Some(current_token_amount),
            ) = (
                bond_map.get(&bond_address),
                interest_map.get(&mint_address),
                metadata_pointer_map.get(&mint_address),
                metadata_map.get(metadata_address),
                current_token_amount_map.get(&mint_address),
            ) {
                bond_details_map.insert(
                    bond_address,
                    BondDetails {
                        bond: bond.clone(),
                        interest_bearing_config: interest_bearing_details.clone(),
                        metadata_pointer: metadata_pointer.clone(),
                        metadata: metadata.clone(),
                        current_token_amount: current_token_amount.clone(),
                    },
                );
            }
        }
    }

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

pub const MAX_EXP_10: usize = 20;
pub const EXP_10: [u64; MAX_EXP_10] = [
    1,                    // 10^0
    10,                   // 10^1
    100,                  // 10^2
    1000,                 // 10^3
    10000,                // 10^4
    100000,               // 10^5
    1000000,              // 10^6
    10000000,             // 10^7
    100000000,            // 10^8
    1000000000,           // 10^9
    10000000000,          // 10^10
    100000000000,         // 10^11
    1000000000000,        // 10^12
    10000000000000,       // 10^13
    100000000000000,      // 10^14
    1000000000000000,     // 10^15
    10000000000000000,    // 10^16
    100000000000000000,   // 10^17
    1000000000000000000,  // 10^18
    10000000000000000000, // 10^19
];
