use generated::types::PaymentFeedType;
use solana_program::pubkey;
use solana_program::pubkey::Pubkey;

#[cfg(feature = "sdk")]
use ::{
    anyhow::{anyhow, Error, Result},
    mpl_token_metadata::accounts::Metadata,
    solana_account_decoder::UiAccountEncoding,
    solana_client::{
        rpc_client::RpcClient,
        rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig},
        rpc_filter::{Memcmp, RpcFilterType},
    },
    solana_sdk::account::ReadableAccount,
    spl_associated_token_account::get_associated_token_address,
    spl_token_2022::{
        extension::{
            interest_bearing_mint::InterestBearingConfig as InterestBearingConfigExt,
            metadata_pointer::MetadataPointer as MetadataPointerExt, BaseStateWithExtensions,
            StateWithExtensions,
        },
        state::Mint,
    },
    std::str::FromStr,
};

#[cfg(feature = "sdk")]
use generated::{
    accounts::{
        AccessPass, Config, Delegate, Issuance, NftIssuanceVault, Payment, PaymentFeed, Payout,
        PurchaseOrder,
    },
    types::Descriminator,
};

#[allow(unused)]
#[allow(clippy::identity_op)]
mod generated;

pub use generated::programs::STABLEBOND_ID;
pub use generated::programs::STABLEBOND_ID as ID;
pub use generated::*;

impl AsRef<[u8]> for PaymentFeedType {
    fn as_ref(&self) -> &[u8] {
        match self {
            Self::UsdcUsd => b"usdc_usd",
            Self::UsdcMxn => b"usdc_mxn",
            Self::SwitchboardUsdcUsd => b"switchboard_usdc_usd",
            Self::SwitchboardUsdcMxn => b"switchboard_usdc_mxn",
            Self::Stub => b"stub",
        }
    }
}

#[cfg(feature = "sdk")]
impl FromStr for PaymentFeedType {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Error> {
        match s {
            "usdc_usd" => Ok(Self::UsdcUsd),
            "usdc_mxn" => Ok(Self::UsdcMxn),
            "switchboard_usdc_usd" => Ok(Self::SwitchboardUsdcUsd),
            "switchboard_usdc_mxn" => Ok(Self::SwitchboardUsdcMxn),
            "stub" => Ok(Self::Stub),
            _ => Err(anyhow!("Invalid payment feed type")),
        }
    }
}

pub fn find_config_pda() -> (Pubkey, u8) {
    Pubkey::find_program_address(&["config".to_string().as_ref()], &crate::ID)
}

pub fn find_delegate_pda(delegate_wallet: Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &["delegate".to_string().as_ref(), delegate_wallet.as_ref()],
        &crate::ID,
    )
}

pub fn find_bond_pda(mint: Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(&["bond".to_string().as_ref(), mint.as_ref()], &crate::ID)
}

pub fn find_issuance_pda(bond: Pubkey, issuance_number: u64) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &[
            "issuance".to_string().as_ref(),
            bond.as_ref(),
            &issuance_number.to_le_bytes(),
        ],
        &crate::ID,
    )
}

pub fn find_payment_pda(issuance: Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &["payment".to_string().as_ref(), issuance.as_ref()],
        &crate::ID,
    )
}

pub fn find_payout_pda(issuance: Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &["payout".to_string().as_ref(), issuance.as_ref()],
        &crate::ID,
    )
}

pub fn find_payment_feed_pda(payment_feed_type: PaymentFeedType) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &[
            "payment_feed".to_string().as_ref(),
            payment_feed_type.as_ref(),
        ],
        &crate::ID,
    )
}

pub fn find_nft_issuance_vault_pda(nft_mint: Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &["nft_issuance_vault".to_string().as_ref(), nft_mint.as_ref()],
        &crate::ID,
    )
}

pub fn find_access_pass_pda(user_wallet: Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &["access_pass".to_string().as_ref(), user_wallet.as_ref()],
        &crate::ID,
    )
}

pub fn find_purchase_order_pda(nft_mint: Pubkey) -> (Pubkey, u8) {
    Pubkey::find_program_address(
        &["purchase_order".to_string().as_ref(), nft_mint.as_ref()],
        &crate::ID,
    )
}

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

#[cfg(feature = "sdk")]
#[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,
}

#[cfg(feature = "sdk")]
#[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,
}

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

#[cfg(feature = "sdk")]
fn remove_null_bytes(s: String) -> String {
    String::from(s.trim_matches(char::from(0)))
}

#[cfg(feature = "sdk")]
fn get_bond_metadata(mint: Pubkey, client: &RpcClient) -> Result<BondMetadata> {
    let mint_account = client.get_account(&mint)?;
    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)?;
    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,
    })
}

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

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

#[cfg(feature = "sdk")]
#[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>>,
}

#[cfg(feature = "sdk")]
#[derive(Debug, Clone, Copy)]
pub struct DescriminatorKey(u8);

#[cfg(feature = "sdk")]
impl From<u8> for DescriminatorKey {
    fn from(value: u8) -> Self {
        DescriminatorKey(value)
    }
}

#[cfg(feature = "sdk")]
impl From<Descriminator> for DescriminatorKey {
    fn from(value: Descriminator) -> Self {
        match value {
            Descriminator::Bond => DescriminatorKey(0),
            Descriminator::Config => DescriminatorKey(1),
            Descriminator::Delegate => DescriminatorKey(2),
            Descriminator::Issuance => DescriminatorKey(3),
            Descriminator::NftIssuanceVault => DescriminatorKey(4),
            Descriminator::Payment => DescriminatorKey(5),
            Descriminator::Payout => DescriminatorKey(6),
            Descriminator::PaymentFeed => DescriminatorKey(7),
            Descriminator::AccessPass => DescriminatorKey(8),
            Descriminator::PurchaseOrder => DescriminatorKey(9),
        }
    }
}

#[cfg(feature = "sdk")]
impl TryFrom<DescriminatorKey> for Descriminator {
    type Error = anyhow::Error;

    fn try_from(value: DescriminatorKey) -> anyhow::Result<Self> {
        match value.0 {
            0 => Ok(Descriminator::Bond),
            1 => Ok(Descriminator::Config),
            2 => Ok(Descriminator::Delegate),
            3 => Ok(Descriminator::Issuance),
            4 => Ok(Descriminator::NftIssuanceVault),
            5 => Ok(Descriminator::Payment),
            6 => Ok(Descriminator::Payout),
            7 => Ok(Descriminator::PaymentFeed),
            8 => Ok(Descriminator::AccessPass),
            9 => Ok(Descriminator::PurchaseOrder),
            _ => Err(anyhow::anyhow!("Invalid account type")),
        }
    }
}

#[cfg(feature = "sdk")]
pub fn get_payment_feed_account(
    payment_feed_type: PaymentFeedType,
    client: &RpcClient,
) -> Result<(Pubkey, PaymentFeed)> {
    let payment_feed_descriminator = Descriminator::PaymentFeed;
    let payment_feed_descriminator_key: DescriminatorKey = payment_feed_descriminator.into();
    let account_type_filter = RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
        0,
        &[payment_feed_descriminator_key.0],
    ));
    let filter_vec = vec![account_type_filter];

    let config = RpcProgramAccountsConfig {
        filters: Some(filter_vec),
        account_config: RpcAccountInfoConfig {
            encoding: Some(UiAccountEncoding::Base64),
            ..RpcAccountInfoConfig::default()
        },
        ..RpcProgramAccountsConfig::default()
    };

    let payment_feed_accounts = client.get_program_accounts_with_config(&ID, config)?;
    for (key, account) in payment_feed_accounts {
        let payment_feed_account = PaymentFeed::from_bytes(account.data())?;
        if payment_feed_account.payment_feed_type == payment_feed_type {
            return Ok((key, payment_feed_account));
        }
    }

    Err(anyhow!("Payment feed account not found"))
}

#[cfg(feature = "sdk")]
pub fn get_program_accounts(client: &RpcClient) -> Result<GetProgramAccountsOutput> {
    let accounts = client.get_program_accounts(&STABLEBOND_ID)?;

    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![];

    for (address, account) in accounts {
        if let Ok(account_type) = Descriminator::try_from(DescriminatorKey::from(account.data()[0]))
        {
            match account_type {
                Descriminator::Bond => {
                    let bond_with_metadata = get_bond_with_metadata(address, &client)?;
                    bonds.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: bond_with_metadata,
                    });
                }
                Descriminator::Config => {
                    configs.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Config::from_bytes(account.data())?,
                    });
                }
                Descriminator::Delegate => {
                    delegates.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Delegate::from_bytes(account.data())?,
                    });
                }
                Descriminator::Issuance => {
                    issuances.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Issuance::from_bytes(account.data())?,
                    });
                }
                Descriminator::NftIssuanceVault => {
                    nft_issuance_vaults.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: NftIssuanceVault::from_bytes(account.data())?,
                    });
                }
                Descriminator::Payment => {
                    payments.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Payment::from_bytes(account.data())?,
                    });
                }
                Descriminator::Payout => {
                    payouts.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: Payout::from_bytes(account.data())?,
                    });
                }
                Descriminator::PaymentFeed => {
                    payment_feeds.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: PaymentFeed::from_bytes(account.data())?,
                    });
                }
                Descriminator::AccessPass => {
                    access_passes.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: AccessPass::from_bytes(account.data())?,
                    });
                }
                Descriminator::PurchaseOrder => {
                    purchase_orders.push(IdentifiedAccount {
                        id: address.to_string(),
                        account: PurchaseOrder::from_bytes(account.data())?,
                    });
                }
            }
        }
    }

    Ok(GetProgramAccountsOutput {
        configs,
        delegates,
        bonds,
        issuances,
        payments,
        payouts,
        nft_issuance_vaults,
        payment_feeds,
        access_passes,
    })
}

#[cfg(feature = "sdk")]
pub fn find_payment_token_address(
    bond_address: Pubkey,
    payment_mint_address: Pubkey,
    issuance_number: u64,
) -> Pubkey {
    let (issuance_address, _) = find_issuance_pda(bond_address, issuance_number);
    let (payment_address, _) = find_payment_pda(issuance_address);
    let payment_token_address =
        get_associated_token_address(&payment_address, &payment_mint_address);
    payment_token_address
}

#[cfg(feature = "sdk")]
pub fn get_payment_token_address(bond_address: Pubkey, client: &RpcClient) -> Result<Pubkey> {
    let bond_with_metadata = get_bond_with_metadata(bond_address, client)?;
    let (_, payment_feed_account) =
        get_payment_feed_account(bond_with_metadata.bond.payment_feed_type, client)?;

    let issuance_number = bond_with_metadata.bond.issuance_number;
    let payment_mint_address = payment_feed_account.payment_mint;

    let payment_token_address =
        find_payment_token_address(bond_address, payment_mint_address, issuance_number);

    Ok(payment_token_address)
}

cfg_if::cfg_if! {
    if #[cfg(feature = "mainnet-beta")] {
        pub const PYTH_PROGRAM: Pubkey = pubkey!("FsJ3A3u2vn5cTVofAjvy6y5kwABJAqYWpe4975bi2epH");
    } else {
        pub const PYTH_PROGRAM: Pubkey = pubkey!("gSbePebfvPy7tRqimPoVecS2UsBvYv46ynrzWocc92s");
    }
}

cfg_if::cfg_if! {
    if #[cfg(feature = "mainnet-beta")] {
        pub const PYTH_USDC: Pubkey = pubkey!("Gnt27xtC473ZT2Mw5u8wZ68Z3gULkSTb5DuxJy7eJotD");
    } else {
        pub const PYTH_USDC: Pubkey = pubkey!("5SSkXsEKQepHHAewytPVwdej4epN1nxgLVM84L4KXgy7");
    }
}

cfg_if::cfg_if! {
    if #[cfg(feature = "mainnet-beta")] {
        pub const PROGRAM_OWNER: Pubkey = pubkey!("owNEred9E1jEEFsS2E3LRMNtAnUf6XBseqBnyKc33Pz");
    } else {
        pub const PROGRAM_OWNER: Pubkey = pubkey!("9Qx7RaqE56rHz4k1bM3Bta2dmqYGd8nLQxonde26MukW");
    }
}
