use solana_transaction_status::{EncodedTransaction, UiMessage};

use crate::{
    accounts::{Issuance, PaymentFeed},
    find_bond_pda, find_issuance_pda, find_payment_feed_pda, find_payout_pda, generated,
};
use ::{
    anyhow::{anyhow, Result},
    rust_decimal::{Decimal, RoundingStrategy},
    solana_client::nonblocking::rpc_client::RpcClient,
    solana_program::pubkey::Pubkey,
    solana_sdk::signature::Signature,
    solana_transaction_status::{
        parse_accounts::ParsedAccount, UiTransactionEncoding, UiTransactionTokenBalance,
    },
    spl_associated_token_account::get_associated_token_address,
    spl_token_2022::state::Mint,
    std::str::FromStr,
};

macro_rules! cast {
    ($target:expr, $pat:path) => {{
        if let $pat(a) = $target {
            Ok(a)
        } else {
            Err(anyhow!(format!(
                "mismatch variant when cast to {}",
                stringify!($pat)
            )))
        }
    }};
}

#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct AccountBalance {
    pub pubkey: Pubkey,
    pub balance: Option<f64>,
}

#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct PayoutDetails {
    pub payment_price: Decimal,
    pub post_token_balance: Decimal,
}

pub async fn get_payout_amount_currency(
    rpc_client: &RpcClient,
    mint: Pubkey,
    issuance_number: u64,
) -> Result<f64> {
    use solana_account_decoder::parse_token::token_amount_to_ui_amount;
    use spl_token_2022::extension::StateWithExtensionsOwned;

    let bond_account = find_bond_pda(mint).0;
    let mint_data = rpc_client.get_account_data(&mint).await?;
    let mint = StateWithExtensionsOwned::<Mint>::unpack(mint_data)?;
    let issuance_account = find_issuance_pda(bond_account, issuance_number).0;
    let issuance_data = rpc_client.get_account_data(&issuance_account).await?;
    let issuance = Issuance::from_bytes(&issuance_data)?;

    let ui_amount_at_maturity =
        token_amount_to_ui_amount(issuance.ending_token_amount, mint.base.decimals);
    let ui_amount_requested_redemptions =
        token_amount_to_ui_amount(issuance.requested_redemptions, mint.base.decimals);

    let amount_at_maturity = ui_amount_at_maturity
        .ui_amount
        .ok_or(anyhow!("ui_amount_at_maturity is None"))?;

    let requested_redemptions = ui_amount_requested_redemptions
        .ui_amount
        .ok_or(anyhow!("ui_amount_requested_redemptions is None"))?;

    let payout_amount_currency = amount_at_maturity * requested_redemptions;

    Ok(payout_amount_currency)
}

pub async fn get_payout_details(
    client: &RpcClient,
    mint: Pubkey,
    issuance_number: u64,
    signature: Signature,
) -> Result<PayoutDetails> {
    let bond_account = find_bond_pda(mint).0;
    let bond_data = client.get_account_data(&bond_account).await?;
    let bond = generated::accounts::Bond::from_bytes(&bond_data)?;

    let payment_feed_account = find_payment_feed_pda(bond.payment_feed_type).0;
    let payment_feed_data = client.get_account_data(&payment_feed_account).await?;
    let payment_feed = PaymentFeed::from_bytes(&payment_feed_data)?;
    let payment_mint_account = payment_feed.payment_mint;

    let issuance_account = find_issuance_pda(bond_account, issuance_number).0;
    let payout_account = find_payout_pda(issuance_account).0;

    let payout_token_account = get_associated_token_address(&payout_account, &payment_mint_account);

    let tx = client
        .get_transaction(&signature, UiTransactionEncoding::JsonParsed)
        .await?;

    let tx = tx.transaction.clone();
    let meta = tx.meta.clone().ok_or(anyhow!("No meta on transaction"))?;
    let transaction = tx.transaction.clone();

    let log_messages: Option<Vec<String>> = meta.log_messages.into();
    let log_messages = log_messages.ok_or(anyhow!("No log messages on transaction"))?;

    let mut payment_price: Option<Decimal> = None;
    log_messages.iter().for_each(|message| {
        let log_prefix = "Program log: Payment Price:";
        if message.contains(log_prefix) {
            let payment_price_str = message.replace(log_prefix, "").trim().to_string();
            let result = Decimal::from_str_exact(&payment_price_str);
            if result.is_ok() {
                payment_price = Some(result.unwrap());
            }
        }
    });

    let json_encoded_transaction = cast!(transaction, EncodedTransaction::Json)?;
    let parsed_ui_message = cast!(json_encoded_transaction.message, UiMessage::Parsed)?;
    let account_keys = parsed_ui_message.account_keys;
    let post_token_balances: Option<Vec<UiTransactionTokenBalance>> =
        meta.post_token_balances.into();
    let post_token_balances = post_token_balances.ok_or(anyhow!("No post token balances found"))?;

    let post_token_balances = get_account_balances(post_token_balances, account_keys.clone())?;

    let post_token_balance = post_token_balances
        .iter()
        .find(|balance| balance.pubkey == payout_token_account)
        .ok_or(anyhow!("payout_token_account not found in transaction"))?
        .balance
        .ok_or(anyhow!("No post_token_balance found"))?;
    let post_token_balance = Decimal::from_f64_retain(post_token_balance)
        .ok_or(anyhow!("Unable to parse post_token_balance"))?
        .round_dp_with_strategy(6, RoundingStrategy::AwayFromZero);

    Ok(PayoutDetails {
        payment_price: payment_price.ok_or(anyhow!("No payment_price found"))?,
        post_token_balance: post_token_balance,
    })
}

fn get_account_balances(
    balances: Vec<UiTransactionTokenBalance>,
    account_keys: Vec<ParsedAccount>,
) -> Result<Vec<AccountBalance>> {
    balances
        .iter()
        .map(|balance| {
            let account = &account_keys[balance.account_index as usize];
            Ok(AccountBalance {
                pubkey: Pubkey::from_str(account.pubkey.as_str())?,
                balance: balance.ui_token_amount.ui_amount,
            })
        })
        .collect::<Result<Vec<AccountBalance>>>()
}
