use anchor_lang::system_program;
use futures_util::StreamExt;
use jito_protos::searcher::SubscribeBundleResultsRequest;
use jito_searcher_client::{
    get_searcher_client, send_bundle_with_confirmation,
};
use log::{debug, error, info, warn};
use solana_account_decoder::UiAccountEncoding;
use solana_sdk::system_instruction::transfer;
use solana_sdk::transaction::{Transaction, VersionedTransaction};
use std::collections::HashMap;
use std::error::Error;
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio::time::{sleep, Duration};
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use solana_client::nonblocking::pubsub_client::PubsubClient;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_client::rpc_config::{
    RpcAccountInfoConfig, RpcSendTransactionConfig, RpcTransactionLogsConfig,
    RpcTransactionLogsFilter,
};
use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel};
use solana_sdk::instruction::{AccountMeta, Instruction};
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Keypair;
use solana_sdk::signer::{EncodableKey, Signer};
use solana_transaction_status::{
    EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, UiMessage,
    UiParsedMessage,
};
use crate::constants::JITO_TIP_PUBKEY;
use crate::get_tx_async_with_client;
use crate::jito::{send_swap_tx_no_wait, SearcherClient};
use crate::raydium::make_compute_budget_ixs;
use crate::util::{env, pubkey_to_string, string_to_pubkey, string_to_u64};
pub const BLOXROUTE_ADDRESS: &str =
    "HWEoBxYs7ssKuudEjzjmpfJVX7Dvi7wescFsVx2L5yoY";
pub const PUMP_GLOBAL_ADDRESS: &str =
    "4wTV1YmiEkRvAtNtsSGPtUrqRYQMe5SKy2uB4Jjaxnjf";
pub const PUMP_FEE_ADDRESS: &str =
    "CebN5WGQ4jvEPvsVU4EoHEpgzq1VV7AbicfhtW4xC9iM";
pub const PUMP_FUN_PROGRAM: &str =
    "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
pub const PUMP_FUN_MINT_AUTHORITY: &str =
    "TSLvdd1pWpHVjahSpsvCXUbgwsL3JAcvokwaKt1eokM";
pub const EVENT_AUTHORITY: &str =
    "Ce6TQqeHC9p8KetsN6JsjHK7UTZk7nasjjnr7XxXp9F1";
pub const PUMP_BUY_METHOD: [u8; 8] =
    [0x66, 0x06, 0x3d, 0x12, 0x01, 0xda, 0xeb, 0xea];
pub const PUMP_SELL_METHOD: [u8; 8] =
    [0x33, 0xe6, 0x85, 0xa4, 0x01, 0x7f, 0x83, 0xad];
pub const TOKEN_PROGRAM: &str = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
pub const RENT_PROGRAM: &str = "SysvarRent111111111111111111111111111111111";
pub const ASSOCIATED_TOKEN_PROGRAM: &str =
    "ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL";
#[derive(BorshSerialize)]
pub struct PumpFunSwapInstructionData {
    pub method_id: [u8; 8],
    pub token_amount: u64,
    pub lamports: u64,
}
#[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
pub struct BondingCurveLayout {
    pub blob1: u64,
    pub virtual_token_reserves: u64,
    pub virtual_sol_reserves: u64,
    pub real_token_reserves: u64,
    pub real_sol_reserves: u64,
    pub blob4: u64,
    pub complete: bool,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PumpTokenData {
    pub address: String,
    pub balance: u64,
    pub image_uri: String,
    pub market_cap: f64,
    pub mint: String,
    pub name: String,
    pub symbol: String,
    pub value: f64,
}
impl BondingCurveLayout {
    pub const LEN: usize = 8 + 8 + 8 + 8 + 8 + 8 + 1;
    pub fn parse(data: &[u8]) -> Result<Self, std::io::Error> {
        Self::try_from_slice(data)
    }
}
pub fn get_local_timestamp() -> chrono::DateTime<chrono::Local> {
    let utc_now = chrono::Utc::now();
    utc_now.with_timezone(&chrono::Local)
}
pub async fn mint_to_pump_accounts(
    mint: &Pubkey,
) -> Result<PumpAccounts, Box<dyn Error>> {
    const PUMP_FUN_PROGRAM: &str =
        "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
    let (bonding_curve, _) = Pubkey::find_program_address(
        &[b"bonding-curve", mint.as_ref()],
        &Pubkey::from_str(PUMP_FUN_PROGRAM)?,
    );
    let associated_bonding_curve =
        spl_associated_token_account::get_associated_token_address(
            &bonding_curve,
            mint,
        );
    Ok(PumpAccounts {
        mint: *mint,
        bonding_curve,
        associated_bonding_curve,
        dev: Pubkey::default(),
        metadata: Pubkey::default(),
    })
}
pub async fn get_tokens_held(
    owner: &Pubkey,
) -> Result<Vec<PumpTokenData>, Box<dyn Error>> {
    let url = "https://frontend-api.pump.fun/balances/{}?limit=100&offset=0";
    let url = url.replace("{}", &owner.to_string());
    Ok(reqwest::get(&url)
        .await?
        .json::<Vec<PumpTokenData>>()
        .await?)
}
pub async fn get_bonding_curve(
    rpc_client: &RpcClient,
    bonding_curve_pubkey: Pubkey,
) -> Result<BondingCurveLayout, Box<dyn Error>> {
    const MAX_RETRIES: u32 = 5;
    const INITIAL_DELAY_MS: u64 = 200;
    let mut retries = 0;
    let mut delay = Duration::from_millis(INITIAL_DELAY_MS);
    loop {
        match rpc_client
            .get_account_with_config(
                &bonding_curve_pubkey,
                RpcAccountInfoConfig {
                    encoding: Some(UiAccountEncoding::Base64),
                    commitment: Some(CommitmentConfig::processed()),
                    data_slice: None,
                    min_context_slot: None,
                },
            )
            .await
        {
            Ok(res) => {
                if let Some(account) = res.value {
                    let data_length = account.data.len();
                    let data: [u8; 49] =
                        account.data.try_into().map_err(|_| {
                            format!("Invalid data length: {}", data_length)
                        })?;
                    debug!("Raw bytes: {:?}", data);
                    let layout = BondingCurveLayout {
                        blob1: u64::from_le_bytes(data[0..8].try_into()?),
                        virtual_token_reserves: u64::from_le_bytes(
                            data[8..16].try_into()?,
                        ),
                        virtual_sol_reserves: u64::from_le_bytes(
                            data[16..24].try_into()?,
                        ),
                        real_token_reserves: u64::from_le_bytes(
                            data[24..32].try_into()?,
                        ),
                        real_sol_reserves: u64::from_le_bytes(
                            data[32..40].try_into()?,
                        ),
                        blob4: u64::from_le_bytes(data[40..48].try_into()?),
                        complete: data[48] != 0,
                    };
                    debug!("Parsed BondingCurveLayout: {:?}", layout);
                    return Ok(layout);
                } else {
                    if retries >= MAX_RETRIES {
                        error!("Max retries reached. Account not found.");
                        return Err(
                            "Account not found after max retries".into()
                        );
                    }
                    warn!(
                        "Attempt {} failed: Account not found. Retrying in {:?}...",
                        retries + 1,
                        delay
                    );
                    sleep(delay).await;
                    retries += 1;
                    delay = Duration::from_millis(
                        INITIAL_DELAY_MS * 2u64.pow(retries),
                    );
                    continue;
                }
            }
            Err(e) => {
                if retries >= MAX_RETRIES {
                    error!("Max retries reached. Last error: {}", e);
                    return Err(format!(
                        "Max retries reached. Last error: {}",
                        e
                    )
                    .into());
                }
                warn!(
                    "Attempt {} failed: {}. Retrying in {:?}...",
                    retries + 1,
                    e,
                    delay
                );
                sleep(delay).await;
                retries += 1;
                delay = Duration::from_millis(
                    INITIAL_DELAY_MS * 2u64.pow(retries),
                );
            }
        }
    }
}
pub fn get_token_amount(
    virtual_sol_reserves: u64,
    virtual_token_reserves: u64,
    real_token_reserves: u64,
    lamports: u64,
) -> Result<u64, Box<dyn std::error::Error>> {
    let virtual_sol_reserves = virtual_sol_reserves as u128;
    let virtual_token_reserves = virtual_token_reserves as u128;
    let amount_in = lamports as u128;
    let reserves_product = virtual_sol_reserves
        .checked_mul(virtual_token_reserves)
        .ok_or("Overflow in reserves product calculation")?;
    let new_virtual_sol_reserve = virtual_sol_reserves
        .checked_add(amount_in)
        .ok_or("Overflow in new virtual SOL reserve calculation")?;
    let new_virtual_token_reserve = reserves_product
        .checked_div(new_virtual_sol_reserve)
        .ok_or("Division by zero or overflow in new virtual token reserve calculation")?
        .checked_add(1)
        .ok_or("Overflow in new virtual token reserve calculation")?;
    let amount_out = virtual_token_reserves
        .checked_sub(new_virtual_token_reserve)
        .ok_or("Underflow in amount out calculation")?;
    let final_amount_out =
        std::cmp::min(amount_out, real_token_reserves as u128);
    Ok(final_amount_out as u64)
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct PumpBuyRequest {
    #[serde(
        serialize_with = "pubkey_to_string",
        deserialize_with = "string_to_pubkey"
    )]
    pub mint: Pubkey,
    #[serde(
        serialize_with = "pubkey_to_string",
        deserialize_with = "string_to_pubkey"
    )]
    pub bonding_curve: Pubkey,
    #[serde(
        serialize_with = "pubkey_to_string",
        deserialize_with = "string_to_pubkey"
    )]
    pub associated_bonding_curve: Pubkey,
    #[serde(deserialize_with = "string_to_u64")]
    pub virtual_token_reserves: u64,
    #[serde(deserialize_with = "string_to_u64")]
    pub virtual_sol_reserves: u64,
    #[serde(deserialize_with = "string_to_u64")]
    pub real_token_reserves: u64,
    #[serde(deserialize_with = "string_to_u64")]
    pub real_sol_reserves: u64,
}
pub async fn instabuy_pump_token(
    wallet: &Keypair,
    lamports: u64,
    searcher_client: &mut Arc<Mutex<SearcherClient>>,
    pump_buy_request: PumpBuyRequest,
) -> Result<(), Box<dyn Error>> {
    let owner = wallet.pubkey();
    let token_amount = get_token_amount(
        pump_buy_request.virtual_sol_reserves,
        pump_buy_request.virtual_token_reserves,
        pump_buy_request.real_token_reserves,
        lamports,
    )?;
    let token_amount = (token_amount as f64 * 0.9) as u64;
    let mut ixs = _make_buy_ixs(
        owner,
        pump_buy_request.mint,
        pump_buy_request.bonding_curve,
        pump_buy_request.associated_bonding_curve,
        token_amount,
        lamports,
    )?;
    let tip = 100000;
    let mut searcher_client = searcher_client.lock().await;
    send_swap_tx_no_wait(
        &mut ixs,
        tip,
        wallet,
        &mut searcher_client,
        &RpcClient::new(env("RPC_URL")),
    )
    .await?;
    Ok(())
}
pub async fn buy_pump_token(
    wallet: &Keypair,
    rpc_client: &RpcClient,
    pump_accounts: PumpAccounts,
    lamports: u64,
    searcher_client: &mut Arc<Mutex<SearcherClient>>,
    use_jito: bool,
) -> Result<(), Box<dyn Error>> {
    let owner = wallet.pubkey();
    let bonding_curve =
        get_bonding_curve(rpc_client, pump_accounts.bonding_curve).await?;
    let token_amount = get_token_amount(
        bonding_curve.virtual_sol_reserves,
        bonding_curve.virtual_token_reserves,
        bonding_curve.real_token_reserves,
        lamports,
    )?;
    let token_amount = (token_amount as f64 * 0.9) as u64;
    info!("buying {}", token_amount);
    let mut ixs = _make_buy_ixs(
        owner,
        pump_accounts.mint,
        pump_accounts.bonding_curve,
        pump_accounts.associated_bonding_curve,
        token_amount,
        lamports,
    )?;
    if use_jito {
        let tip = 100000;
        let mut searcher_client = searcher_client.lock().await;
        send_swap_tx_no_wait(
            &mut ixs,
            tip,
            wallet,
            &mut searcher_client,
            rpc_client,
        )
        .await?;
    } else {
        _send_tx_standard(ixs, wallet, rpc_client, owner).await?;
    }
    Ok(())
}
pub fn _make_buy_ixs(
    owner: Pubkey,
    mint: Pubkey,
    bonding_curve: Pubkey,
    associated_bonding_curve: Pubkey,
    token_amount: u64,
    lamports: u64,
) -> Result<Vec<Instruction>, Box<dyn Error>> {
    let mut ixs = vec![];
    ixs.append(&mut make_compute_budget_ixs(262500, 100000));
    let ata = spl_associated_token_account::get_associated_token_address(
        &owner, &mint,
    );
    let mut ata_ixs = raydium_library::common::create_ata_token_or_not(
        &owner, &mint, &owner,
    );
    ixs.append(&mut ata_ixs);
    ixs.push(make_pump_swap_ix(
        owner,
        mint,
        bonding_curve,
        associated_bonding_curve,
        token_amount,
        lamports,
        ata,
    )?);
    Ok(ixs)
}
async fn _send_tx_standard(
    ixs: Vec<Instruction>,
    wallet: &Keypair,
    rpc_client: &RpcClient,
    owner: Pubkey,
) -> Result<(), Box<dyn Error>> {
    let transaction =
        VersionedTransaction::from(Transaction::new_signed_with_payer(
            &ixs,
            Some(&owner),
            &[wallet],
            rpc_client.get_latest_blockhash().await?,
        ));
    let res = rpc_client
        .send_transaction_with_config(
            &transaction,
            RpcSendTransactionConfig {
                skip_preflight: true,
                min_context_slot: None,
                preflight_commitment: Some(CommitmentLevel::Processed),
                max_retries: None,
                encoding: None,
            },
        )
        .await;
    match res {
        Ok(sig) => {
            info!("Transaction sent: {}", sig);
        }
        Err(e) => {
            return Err(e.into());
        }
    }
    Ok(())
}
pub async fn sell_pump_token(
    wallet: &Keypair,
    rpc_client: &RpcClient,
    pump_accounts: PumpAccounts,
    token_amount: u64,
) -> Result<(), Box<dyn Error>> {
    let owner = wallet.pubkey();
    let ata = spl_associated_token_account::get_associated_token_address(
        &owner,
        &pump_accounts.mint,
    );
    let mut ixs = vec![];
    ixs.append(&mut make_compute_budget_ixs(262500, 100000));
    ixs.push(make_pump_sell_ix(owner, pump_accounts, token_amount, ata)?);
    let recent_blockhash = rpc_client.get_latest_blockhash().await?;
    let transaction = Transaction::new_signed_with_payer(
        &ixs,
        Some(&owner),
        &[wallet],
        recent_blockhash,
    );
    let res = rpc_client
        .send_transaction_with_config(
            &transaction,
            RpcSendTransactionConfig {
                skip_preflight: true,
                min_context_slot: None,
                preflight_commitment: Some(CommitmentLevel::Processed),
                max_retries: None,
                encoding: None,
            },
        )
        .await;
    match res {
        Ok(sig) => {
            info!("Transaction sent: {}", sig);
        }
        Err(e) => {
            return Err(e.into());
        }
    }
    Ok(())
}
pub fn make_pump_sell_ix(
    owner: Pubkey,
    pump_accounts: PumpAccounts,
    token_amount: u64,
    ata: Pubkey,
) -> Result<Instruction, Box<dyn Error>> {
    let accounts: [AccountMeta; 12] = [
        AccountMeta::new_readonly(
            Pubkey::from_str(PUMP_GLOBAL_ADDRESS)?,
            false,
        ),
        AccountMeta::new(Pubkey::from_str(PUMP_FEE_ADDRESS)?, false),
        AccountMeta::new_readonly(pump_accounts.mint, false),
        AccountMeta::new(pump_accounts.bonding_curve, false),
        AccountMeta::new(pump_accounts.associated_bonding_curve, false),
        AccountMeta::new(ata, false),
        AccountMeta::new(owner, true),
        AccountMeta::new_readonly(system_program::ID, false),
        AccountMeta::new_readonly(
            Pubkey::from_str(ASSOCIATED_TOKEN_PROGRAM)?,
            false,
        ),
        AccountMeta::new_readonly(Pubkey::from_str(TOKEN_PROGRAM)?, false),
        AccountMeta::new_readonly(Pubkey::from_str(EVENT_AUTHORITY)?, false),
        AccountMeta::new_readonly(Pubkey::from_str(PUMP_FUN_PROGRAM)?, false),
    ];
    let data = PumpFunSwapInstructionData {
        method_id: PUMP_SELL_METHOD,
        token_amount,
        lamports: 0,
    };
    Ok(Instruction::new_with_borsh(
        Pubkey::from_str(PUMP_FUN_PROGRAM)?,
        &data,
        accounts.to_vec(),
    ))
}
pub fn make_pump_swap_ix(
    owner: Pubkey,
    mint: Pubkey,
    bonding_curve: Pubkey,
    associated_bonding_curve: Pubkey,
    token_amount: u64,
    lamports: u64,
    ata: Pubkey,
) -> Result<Instruction, Box<dyn Error>> {
    let accounts: [AccountMeta; 12] = [
        AccountMeta::new_readonly(
            Pubkey::from_str(PUMP_GLOBAL_ADDRESS)?,
            false,
        ),
        AccountMeta::new(Pubkey::from_str(PUMP_FEE_ADDRESS)?, false),
        AccountMeta::new_readonly(mint, false),
        AccountMeta::new(bonding_curve, false),
        AccountMeta::new(associated_bonding_curve, false),
        AccountMeta::new(ata, false),
        AccountMeta::new(owner, true),
        AccountMeta::new_readonly(system_program::ID, false),
        AccountMeta::new_readonly(Pubkey::from_str(TOKEN_PROGRAM)?, false),
        AccountMeta::new_readonly(Pubkey::from_str(RENT_PROGRAM)?, false),
        AccountMeta::new_readonly(Pubkey::from_str(EVENT_AUTHORITY)?, false),
        AccountMeta::new_readonly(Pubkey::from_str(PUMP_FUN_PROGRAM)?, false),
    ];
    let data = PumpFunSwapInstructionData {
        method_id: PUMP_BUY_METHOD,
        token_amount,
        lamports,
    };
    Ok(Instruction::new_with_borsh(
        Pubkey::from_str(PUMP_FUN_PROGRAM)?,
        &data,
        accounts.to_vec(),
    ))
}
pub async fn snipe_pump(only_listen: bool) -> Result<(), Box<dyn Error>> {
    let wallet = Arc::new(
        Keypair::read_from_file(env("FUND_KEYPAIR_PATH"))
            .expect("read wallet"),
    );
    let rpc_client = Arc::new(RpcClient::new(env("RPC_URL")));
    let auth =
        Arc::new(Keypair::read_from_file(env("AUTH_KEYPAIR_PATH")).unwrap());
    let searcher_client = Arc::new(Mutex::new(
        get_searcher_client(env("BLOCK_ENGINE_URL").as_str(), &auth)
            .await
            .expect("makes searcher client"),
    ));
    let client = PubsubClient::new(&env("WS_URL"))
        .await
        .expect("pubsub client async");
    let (mut notifications, unsub) = client
        .logs_subscribe(
            RpcTransactionLogsFilter::Mentions(vec![
                PUMP_FUN_MINT_AUTHORITY.to_string()
            ]),
            RpcTransactionLogsConfig {
                commitment: Some(CommitmentConfig::processed()),
            },
        )
        .await
        .expect("subscribe to logs");
    info!("Listening for PumpFun events");
    let mut cache = HashMap::<String, bool>::new();
    while let Some(log) = notifications.next().await {
        let sig = log.value.signature;
        let tx = match get_tx_async_with_client(&rpc_client, &sig, 5).await {
            Ok(tx) => tx,
            Err(_) => {
                warn!("did not get tx in time");
                continue;
            }
        };
        let slot = tx.slot;
        let accounts = parse_pump_accounts(tx)?;
        info!(
            "PumpFun shitter: {} (slot: {})",
            accounts.mint.to_string(),
            slot,
        );
        if only_listen {
            continue;
        }
        let mint = accounts.mint.to_string();
        if cache.contains_key(&mint) {
            info!("Already bought {} shitter", mint);
            continue;
        }
        cache.insert(mint.clone(), true);
        let metadata = fetch_metadata(&accounts.mint)
            .await
            .expect("fetch_metadata");
        if metadata.website.is_none() {
            warn!("No website for {}", mint);
            continue;
        }
        if metadata.twitter.is_none() {
            warn!("No twitter for {}", mint);
            continue;
        }
        if metadata.telegram.is_none() {
            warn!("No telegram for {}", mint);
            continue;
        }
        let website = metadata.website.unwrap();
        let twitter = metadata.twitter.unwrap();
        let telegram = metadata.telegram.unwrap();
        if website == twitter || website == telegram || twitter == telegram {
            warn!("Same link for all socials for {}", mint);
            continue;
        }
        let wallet_clone = Arc::clone(&wallet);
        let rpc_client_clone = Arc::clone(&rpc_client);
        let mut searcher_client = Arc::clone(&searcher_client);
        tokio::spawn(async move {
            let result = buy_pump_token(
                &wallet_clone,
                &rpc_client_clone,
                accounts,
                1_000_000,
                &mut searcher_client,
                true, )
            .await;
            if let Err(e) = result {
                error!("Error buying pump token: {:?}", e);
            }
        });
    }
    unsub().await;
    Ok(())
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct PumpAccounts {
    #[serde(
        serialize_with = "pubkey_to_string",
        deserialize_with = "string_to_pubkey"
    )]
    pub mint: Pubkey,
    #[serde(
        serialize_with = "pubkey_to_string",
        deserialize_with = "string_to_pubkey"
    )]
    pub bonding_curve: Pubkey,
    #[serde(
        serialize_with = "pubkey_to_string",
        deserialize_with = "string_to_pubkey"
    )]
    pub associated_bonding_curve: Pubkey,
    #[serde(
        serialize_with = "pubkey_to_string",
        deserialize_with = "string_to_pubkey"
    )]
    pub dev: Pubkey,
    #[serde(
        serialize_with = "pubkey_to_string",
        deserialize_with = "string_to_pubkey"
    )]
    pub metadata: Pubkey,
}
pub fn parse_pump_accounts(
    tx: EncodedConfirmedTransactionWithStatusMeta,
) -> Result<PumpAccounts, Box<dyn Error>> {
    if let EncodedTransaction::Json(tx) = &tx.transaction.transaction {
        if let UiMessage::Parsed(UiParsedMessage {
            account_keys,
            instructions: _,
            recent_blockhash: _,
            address_table_lookups: _,
        }) = &tx.message
        {
            debug!("Account keys: {:?}", account_keys);
            if account_keys.len() >= 5 {
                let dev = account_keys[0].pubkey.parse()?;
                let mint = account_keys[1].pubkey.parse()?;
                let bonding_curve = account_keys[3].pubkey.parse()?;
                let associated_bonding_curve =
                    account_keys[4].pubkey.parse()?;
                let metadata = account_keys[5].pubkey.parse()?;
                Ok(PumpAccounts {
                    mint,
                    bonding_curve,
                    associated_bonding_curve,
                    dev,
                    metadata,
                })
            } else {
                Err("Not enough account keys".into())
            }
        } else {
            Err("Not a parsed transaction".into())
        }
    } else {
        Err("Not a JSON transaction".into())
    }
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PumpTokenInfo {
    pub associated_bonding_curve: String,
    pub bonding_curve: String,
    pub complete: bool,
    pub created_timestamp: i64,
    pub creator: String,
    pub description: String,
    pub image_uri: String,
    pub inverted: bool,
    pub is_currently_live: bool,
    pub king_of_the_hill_timestamp: i64,
    pub last_reply: i64,
    pub market_cap: f64,
    pub market_id: String,
    pub metadata_uri: String,
    pub mint: String,
    pub name: String,
    pub nsfw: bool,
    pub profile_image: Option<String>,
    pub raydium_pool: String,
    pub reply_count: i32,
    pub show_name: bool,
    pub symbol: String,
    pub telegram: Option<String>,
    pub total_supply: i64,
    pub twitter: Option<String>,
    pub usd_market_cap: f64,
    pub username: Option<String>,
    pub virtual_sol_reserves: i64,
    pub virtual_token_reserves: i64,
    pub website: Option<String>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct IPFSMetadata {
    pub name: String,
    pub symbol: String,
    pub description: String,
    pub image: String,
    #[serde(rename = "showName")]
    pub show_name: Option<bool>,
    #[serde(rename = "createdOn")]
    pub created_on: Option<String>,
    pub twitter: Option<String>,
    pub telegram: Option<String>,
    pub website: Option<String>,
}
pub async fn fetch_metadata(
    mint: &Pubkey,
) -> Result<PumpTokenInfo, Box<dyn Error>> {
    const MAX_RETRIES: u32 = 3;
    const INITIAL_DELAY_MS: u64 = 100;
    let mut retry_count = 0;
    let mut delay_ms = INITIAL_DELAY_MS;
    loop {
        match fetch_metadata_inner(mint).await {
            Ok(metadata) => {
                info!("Metadata fetched successfully");
                return Ok(metadata);
            }
            Err(e) => {
                if retry_count >= MAX_RETRIES {
                    info!("Failed to fetch metadata after all retries");
                    return Err(e);
                }
                info!(
                    "Retry attempt {} failed: {:?}. Retrying in {} ms...",
                    retry_count + 1,
                    e,
                    delay_ms
                );
                tokio::time::sleep(Duration::from_millis(delay_ms)).await;
                retry_count += 1;
                delay_ms *= 2; }
        }
    }
}
async fn fetch_metadata_inner(
    mint: &Pubkey,
) -> Result<PumpTokenInfo, Box<dyn Error>> {
    let url = format!("https://frontend-api.pump.fun/coins/{}", mint);
    info!("Fetching metadata from: {}", url);
    let res = reqwest::get(&url).await?;
    info!("res: {:?}", res);
    let data = res.json::<PumpTokenInfo>().await?;
    Ok(data)
}
pub async fn send_pump_bump(
    wallet: &Keypair,
    rpc_client: &RpcClient,
    mint: &Pubkey,
    searcher_client: &mut Arc<Mutex<SearcherClient>>,
    wait_for_confirmation: bool,
) -> Result<(), Box<dyn Error>> {
    let lamports = 22_800_000;
    let owner = wallet.pubkey();
    let pump_accounts = mint_to_pump_accounts(mint).await?;
    let bonding_curve =
        get_bonding_curve(rpc_client, pump_accounts.bonding_curve).await?;
    let token_amount = get_token_amount(
        bonding_curve.virtual_sol_reserves,
        bonding_curve.virtual_token_reserves,
        bonding_curve.real_token_reserves,
        lamports,
    )?;
    let token_amount = (token_amount as f64 * 0.9) as u64;
    let ata = spl_associated_token_account::get_associated_token_address(
        &owner,
        &pump_accounts.mint,
    );
    if rpc_client.get_account(&ata).await.is_err() {
        warn!("ata does not exist, creating it through buy and sell");
        buy_pump_token(
            wallet,
            rpc_client,
            pump_accounts,
            lamports,
            searcher_client,
            false,
        )
        .await?;
        sell_pump_token(wallet, rpc_client, pump_accounts, token_amount)
            .await?;
        return Ok(());
    }
    let mut ixs = vec![];
    ixs.push(make_pump_swap_ix(
        owner,
        pump_accounts.mint,
        pump_accounts.bonding_curve,
        pump_accounts.associated_bonding_curve,
        token_amount,
        lamports,
        ata,
    )?);
    ixs.push(make_pump_sell_ix(owner, pump_accounts, token_amount, ata)?);
    let tip = 50_000;
    ixs.push(transfer(&owner, &Pubkey::from_str(JITO_TIP_PUBKEY)?, tip));
    let tx = VersionedTransaction::from(Transaction::new_signed_with_payer(
        &ixs,
        Some(&owner),
        &[wallet],
        rpc_client.get_latest_blockhash().await?,
    ));
    let mut searcher_client = searcher_client.lock().await;
    if wait_for_confirmation {
        let mut bundle_results_subscription = searcher_client
            .subscribe_bundle_results(SubscribeBundleResultsRequest {})
            .await
            .expect("subscribe to bundle results")
            .into_inner();
        send_bundle_with_confirmation(
            &[tx],
            rpc_client,
            &mut searcher_client,
            &mut bundle_results_subscription,
        )
        .await?;
    } else {
        send_swap_tx_no_wait(
            &mut ixs,
            tip,
            wallet,
            &mut searcher_client,
            rpc_client,
        )
        .await?;
    }
    Ok(())
}
#[cfg(test)]
mod tests {
    use super::*;
    #[tokio::test]
    async fn test_pump_bump() {
        dotenv::from_filename(".env").unwrap();
        let wallet =
            Keypair::read_from_file("./wtf.json").expect("read wallet");
        let rpc_client =
            RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
        let mint =
            Pubkey::from_str("8ALbiQ2aWD4V63bx6s5qtf21LA4r9uBaY2THbg9epump")
                .unwrap();
        let auth = Arc::new(
            Keypair::read_from_file(env("AUTH_KEYPAIR_PATH")).unwrap(),
        );
        let mut searcher_client = Arc::new(Mutex::new(
            get_searcher_client(env("BLOCK_ENGINE_URL").as_str(), &auth)
                .await
                .expect("makes searcher client"),
        ));
        send_pump_bump(
            &wallet,
            &rpc_client,
            &mint,
            &mut searcher_client,
            true,
        )
        .await
        .expect("send_pump_bump");
    }
    #[tokio::test]
    async fn test_fetch_metadata() {
        let metadata = fetch_metadata(
            &Pubkey::from_str("4cRkQ2dntpusYag6Zmvco8T78WxK9Jqh1eEZJox8pump")
                .expect("parse mint"),
        )
        .await
        .expect("fetch_metadata");
        assert_eq!(metadata.name, "🗿".to_string());
        assert_eq!(metadata.symbol, "🗿".to_string());
        assert_eq!(
            metadata.image_uri, "https://cf-ipfs.com/ipfs/QmXn5xkUMxNQ5c5Sfct8rFTq9jNi6jsSHm1yLY2nQyeSke".to_string()
        );
        assert_eq!(
            metadata.twitter,
            Some("https://x.com/thefirstgigasol".to_string())
        );
        assert_eq!(
            metadata.telegram,
            Some("https://t.me/+keptGgOKxN45YWRl".to_string())
        );
        assert_eq!(
            metadata.website,
            Some("https://thefirstgiga.com/".to_string())
        );
    }
    #[test]
    fn test_parse_pump_accounts() {
        let sample_tx =
            std::fs::read_to_string("pump_fun_tx.json").expect("read tx");
        let tx: EncodedConfirmedTransactionWithStatusMeta =
            serde_json::from_str(&sample_tx).expect("parse tx");
        let accounts = parse_pump_accounts(tx).expect("parse accounts");
        println!("{:?}", accounts);
        assert!(
            accounts.mint.to_string()
                == "6kPvKNrLqg23mApAvHzMKWohhVdSrA54HvrpYud8pump"
        );
        assert!(
            accounts.bonding_curve.to_string()
                == "6TGz5VAFF6UpSmTSk9327utugSWJCyVeVVFXDtZnMtNp"
        );
        assert!(
            accounts.associated_bonding_curve.to_string()
                == "4VwNGUif2ubbPjx4YNHmxEH7L4Yt2QFeo8uVTrVC3F68"
        );
        assert!(
            accounts.dev.to_string()
                == "2wgo94ZaiUNUkFBSKNaKsUgEANgSdex7gRpFKR39DPzw"
        );
    }
    #[tokio::test]
    async fn test_buy_pump_token() {
        dotenv::from_filename(".env").unwrap();
        let lamports = 690000;
        let pump_accounts = PumpAccounts {
            mint: Pubkey::from_str(
                "5KEDcNGebCcLptWzknqVmPRNLHfiHA9Mm2djVE26pump",
            )
            .expect("parse mint"),
            bonding_curve: Pubkey::from_str(
                "Drhj4djqLsPyiA9qK2YmBngteFba8XhhvuQoBToW6pMS",
            )
            .expect("parse bonding curve"),
            associated_bonding_curve: Pubkey::from_str(
                "7uXq8diH862Dh8NgMHt5Tzsai8SvURhH58rArgxvs7o1",
            )
            .expect("parse associated bonding curve"),
            dev: Pubkey::from_str(
                "Gizxxed4uXCzL7Q8DyALDVoEEDfMkSV7XyUNrPDnPJ9J",
            )
            .expect("parse associated user"),
            metadata: Pubkey::default(), };
        let wallet =
            Keypair::read_from_file("./fuck.json").expect("read wallet");
        let rpc_client =
            RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
        let auth = Arc::new(
            Keypair::read_from_file(env("AUTH_KEYPAIR_PATH")).unwrap(),
        );
        let mut searcher_client = Arc::new(Mutex::new(
            get_searcher_client(env("BLOCK_ENGINE_URL").as_str(), &auth)
                .await
                .expect("makes searcher client"),
        ));
        buy_pump_token(
            &wallet,
            &rpc_client,
            pump_accounts,
            lamports,
            &mut searcher_client,
            true,
        )
        .await
        .expect("buy pump token");
    }
    #[tokio::test]
    async fn test_get_bonding_curve_incomplete() {
        let rpc_client =
            RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
        let bonding_curve_pubkey = Pubkey::from_str(
            "Drhj4djqLsPyiA9qK2YmBngteFba8XhhvuQoBToW6pMS", )
        .expect("parse bonding curve");
        let bonding_curve =
            get_bonding_curve(&rpc_client, bonding_curve_pubkey)
                .await
                .expect("get bonding curve");
        println!("{:?}", bonding_curve);
        assert!(!bonding_curve.complete);
        assert_ne!(bonding_curve.virtual_token_reserves, 0);
        assert_ne!(bonding_curve.virtual_sol_reserves, 0);
        assert_ne!(bonding_curve.real_token_reserves, 0);
    }
    #[tokio::test]
    async fn test_get_bonding_curve_complete() {
        let rpc_client =
            RpcClient::new("https://api.mainnet-beta.solana.com".to_string());
        let bonding_curve_pubkey = Pubkey::from_str(
            "EB5tQ64HwNjaEoKKYAPkZqndwbULX249EuWSnkjfvR3y", )
        .expect("parse bonding curve");
        let bonding_curve =
            get_bonding_curve(&rpc_client, bonding_curve_pubkey)
                .await
                .expect("get bonding curve");
        println!("{:?}", bonding_curve);
        assert!(bonding_curve.complete);
        assert_eq!(bonding_curve.virtual_token_reserves, 0);
        assert_eq!(bonding_curve.virtual_sol_reserves, 0);
        assert_eq!(bonding_curve.real_token_reserves, 0);
    }
    #[tokio::test]
    async fn test_get_token_amount() {
        let bonding_curve = BondingCurveLayout {
            blob1: 6966180631402821399,
            virtual_token_reserves: 1072964268463317,
            virtual_sol_reserves: 30000999057,
            real_token_reserves: 793064268463317,
            real_sol_reserves: 999057,
            blob4: 1000000000000000,
            complete: false,
        };
        let lamports = 500000;
        let expected_token_amount = 17852389307u64;
        let token_amount = get_token_amount(
            bonding_curve.virtual_sol_reserves,
            bonding_curve.virtual_token_reserves,
            bonding_curve.real_token_reserves,
            lamports,
        )
        .expect("get token amount");
        let low_thresh = 0.9 * expected_token_amount as f64;
        let high_thresh = 1.1 * expected_token_amount as f64;
        let token_amount = token_amount as f64;
        assert!(token_amount >= low_thresh);
        assert!(token_amount <= high_thresh);
    }
}