use super::common::{DiscriminatorKey, PaginatedResponse};
use crate::{
    accounts::{Bond, PaymentFeed},
    find_issuance_pda, find_payment_pda,
    types::{Discriminator, PaymentFeedType},
    STABLEBOND_ID,
};
use anyhow::{anyhow, Error, Result};
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_associated_token_account::get_associated_token_address;
use std::str::FromStr;

#[derive(Debug, Clone, Copy)]
pub struct PaymentFeedTypeKey(u8);

impl Into<u8> for PaymentFeedTypeKey {
    fn into(self) -> u8 {
        self.0
    }
}

impl From<u8> for PaymentFeedTypeKey {
    fn from(value: u8) -> Self {
        PaymentFeedTypeKey(value)
    }
}

impl From<PaymentFeedType> for PaymentFeedTypeKey {
    fn from(value: PaymentFeedType) -> Self {
        match value {
            PaymentFeedType::UsdcUsd => PaymentFeedTypeKey(0),
            PaymentFeedType::UsdcMxn => PaymentFeedTypeKey(1),
            PaymentFeedType::SwitchboardUsdcUsd => PaymentFeedTypeKey(2),
            PaymentFeedType::SwitchboardUsdcMxn => PaymentFeedTypeKey(3),
            PaymentFeedType::Stub => PaymentFeedTypeKey(4),
            PaymentFeedType::SwitchboardUsdcBrl => PaymentFeedTypeKey(5),
            PaymentFeedType::SwitchboardUsdcEur => PaymentFeedTypeKey(6),
            PaymentFeedType::SwitchboardUsdcGbp => PaymentFeedTypeKey(7),
            PaymentFeedType::SwitchboardOnDemandUsdcMxn => PaymentFeedTypeKey(8),
            PaymentFeedType::SwitchboardOnDemandUsdcBrl => PaymentFeedTypeKey(9),
            PaymentFeedType::SwitchboardOnDemandUsdcEur => PaymentFeedTypeKey(10),
            PaymentFeedType::SwitchboardOnDemandUsdcGbp => PaymentFeedTypeKey(11),
            PaymentFeedType::SwitchboardOnDemandUsdcUsd => PaymentFeedTypeKey(12),
        }
    }
}

impl TryFrom<PaymentFeedTypeKey> for PaymentFeedType {
    type Error = anyhow::Error;

    fn try_from(value: PaymentFeedTypeKey) -> anyhow::Result<Self> {
        match value.0 {
            0 => Ok(PaymentFeedType::UsdcUsd),
            1 => Ok(PaymentFeedType::UsdcMxn),
            2 => Ok(PaymentFeedType::SwitchboardUsdcUsd),
            3 => Ok(PaymentFeedType::SwitchboardUsdcMxn),
            4 => Ok(PaymentFeedType::Stub),
            5 => Ok(PaymentFeedType::SwitchboardUsdcBrl),
            6 => Ok(PaymentFeedType::SwitchboardUsdcEur),
            7 => Ok(PaymentFeedType::SwitchboardUsdcGbp),
            8 => Ok(PaymentFeedType::SwitchboardOnDemandUsdcMxn),
            9 => Ok(PaymentFeedType::SwitchboardOnDemandUsdcBrl),
            10 => Ok(PaymentFeedType::SwitchboardOnDemandUsdcEur),
            11 => Ok(PaymentFeedType::SwitchboardOnDemandUsdcGbp),
            12 => Ok(PaymentFeedType::SwitchboardOnDemandUsdcUsd),
            _ => Err(anyhow::anyhow!("Invalid account type")),
        }
    }
}

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),
            "switchboard_usdc_brl" => Ok(Self::SwitchboardUsdcBrl),
            "switchboard_usdc_eur" => Ok(Self::SwitchboardUsdcEur),
            "switchboard_usdc_gbp" => Ok(Self::SwitchboardUsdcGbp),
            "switchboard_on_demand_usdc_mxn" => Ok(Self::SwitchboardOnDemandUsdcMxn),
            "switchboard_on_demand_usdc_brl" => Ok(Self::SwitchboardOnDemandUsdcBrl),
            "switchboard_on_demand_usdc_eur" => Ok(Self::SwitchboardOnDemandUsdcEur),
            "switchboard_on_demand_usdc_gbp" => Ok(Self::SwitchboardOnDemandUsdcGbp),
            "switchboard_on_demand_usdc_usd" => Ok(Self::SwitchboardOnDemandUsdcUsd),
            _ => Err(anyhow!("Invalid payment feed type")),
        }
    }
}

impl std::fmt::Display for PaymentFeedType {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            PaymentFeedType::UsdcUsd => write!(f, "usdc_usd"),
            PaymentFeedType::UsdcMxn => write!(f, "usdc_mxn"),
            PaymentFeedType::SwitchboardUsdcUsd => write!(f, "switchboard_usdc_usd"),
            PaymentFeedType::SwitchboardUsdcMxn => write!(f, "switchboard_usdc_mxn"),
            PaymentFeedType::Stub => write!(f, "stub"),
            PaymentFeedType::SwitchboardUsdcBrl => write!(f, "switchboard_usdc_brl"),
            PaymentFeedType::SwitchboardUsdcEur => write!(f, "switchboard_usdc_eur"),
            PaymentFeedType::SwitchboardUsdcGbp => write!(f, "switchboard_usdc_gbp"),
            PaymentFeedType::SwitchboardOnDemandUsdcMxn => {
                write!(f, "switchboard_on_demand_usdc_mxn")
            }
            PaymentFeedType::SwitchboardOnDemandUsdcBrl => {
                write!(f, "switchboard_on_demand_usdc_brl")
            }
            PaymentFeedType::SwitchboardOnDemandUsdcEur => {
                write!(f, "switchboard_on_demand_usdc_eur")
            }
            PaymentFeedType::SwitchboardOnDemandUsdcGbp => {
                write!(f, "switchboard_on_demand_usdc_gbp")
            }
            PaymentFeedType::SwitchboardOnDemandUsdcUsd => {
                write!(f, "switchboard_on_demand_usdc_usd")
            }
        }
    }
}

pub async fn get_payment_feed_by_payment_feed_type(
    client: &RpcClient,
    payment_feed_type: PaymentFeedType,
) -> Result<(String, PaymentFeed)> {
    let payment_feed_discriminator_key: DiscriminatorKey = Discriminator::PaymentFeed.into();
    let payment_feed_type_key: PaymentFeedTypeKey = payment_feed_type.into();
    let filter_vec = vec![
        RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
            0,
            &[payment_feed_discriminator_key.into()],
        )),
        RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
            99,
            &[payment_feed_type_key.into()],
        )),
    ];

    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(&STABLEBOND_ID, config)
        .await?;

    let (payment_feed_address, payment_feed_account) = payment_feed_accounts
        .first()
        .ok_or_else(|| anyhow!("Payment feed account not found"))?;
    let payment_feed = PaymentFeed::from_bytes(payment_feed_account.data())?;

    Ok((payment_feed_address.to_string(), payment_feed))
}

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

    let payment_feed_index = get_payment_feed_index(client).await?;
    let total_items = payment_feed_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;
    }

    let payment_feed_addresses = payment_feed_index
        .into_iter()
        .map(|(address, _)| address)
        .collect::<Vec<Pubkey>>();

    let items =
        get_payment_feeds_by_addresses(client, &payment_feed_addresses[start_index..end_index])
            .await?;

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

async fn get_payment_feeds_by_addresses(
    client: &RpcClient,
    payment_feed_addresses: &[Pubkey],
) -> Result<Vec<(String, PaymentFeed)>> {
    let payment_feed_accounts = client.get_multiple_accounts(payment_feed_addresses).await?;
    let mut payment_feeds = Vec::<(String, PaymentFeed)>::new();
    for (index, payment_feed_account) in payment_feed_accounts.into_iter().enumerate() {
        if let Some(account) = payment_feed_account {
            let payment_feed = PaymentFeed::from_bytes(account.data())?;
            payment_feeds.push((payment_feed_addresses[index].to_string(), payment_feed));
        }
    }
    Ok(payment_feeds)
}

pub async fn get_payment_feed_index(client: &RpcClient) -> Result<Vec<(Pubkey, PaymentFeedType)>> {
    let payment_feed_discriminator_key: DiscriminatorKey = Discriminator::PaymentFeed.into();
    let filter_vec = vec![RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
        0,
        &[payment_feed_discriminator_key.into()],
    ))];

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

    let payment_feed_accounts = client
        .get_program_accounts_with_config(&STABLEBOND_ID, config)
        .await?;

    let mut payment_feed_addresses_with_keys: Vec<(Pubkey, u8)> = Vec::new();
    for (payment_feed_address, payment_feed_account_slice) in payment_feed_accounts {
        if let Ok(data_slice) = payment_feed_account_slice.data[0..1].try_into() {
            let payment_feed_type_key = u8::from_le_bytes(data_slice);
            payment_feed_addresses_with_keys.push((payment_feed_address, payment_feed_type_key));
        }
    }
    payment_feed_addresses_with_keys.sort_by(|a, b| a.1.cmp(&b.1));

    let mut payment_feed_index = Vec::<(Pubkey, PaymentFeedType)>::new();
    for (address, key) in payment_feed_addresses_with_keys {
        let payment_feed_type_key: PaymentFeedTypeKey = key.into();
        let payment_feed_type: Result<PaymentFeedType, _> = payment_feed_type_key.try_into();
        if let Ok(payment_feed_type) = payment_feed_type {
            payment_feed_index.push((address, payment_feed_type));
        }
    }
    Ok(payment_feed_index)
}

pub async fn get_payment_feed_account(
    payment_feed_type: PaymentFeedType,
    client: &RpcClient,
) -> Result<(String, PaymentFeed)> {
    let payment_feed_discriminator = Discriminator::PaymentFeed;
    let payment_feed_discriminator_key: DiscriminatorKey = payment_feed_discriminator.into();
    let account_type_filter = RpcFilterType::Memcmp(Memcmp::new_base58_encoded(
        0,
        &[payment_feed_discriminator_key.into()],
    ));
    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(&STABLEBOND_ID, config)
        .await?;
    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.to_string(), payment_feed_account));
        }
    }

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

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
}

pub async fn get_payment_token_address(bond_address: Pubkey, client: &RpcClient) -> Result<Pubkey> {
    let bond_account = client.get_account(&bond_address).await?;
    let bond = Bond::from_bytes(&bond_account.data)?;
    let (_, payment_feed_account) =
        get_payment_feed_account(bond.payment_feed_type, client).await?;

    let issuance_number = 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)
}
