use std::collections::HashMap;
use std::str::FromStr;
use log::{debug, error, info, warn};
use raydium_library::amm;
use raydium_library::amm::AmmKeys;
use raydium_library::amm::MarketPubkeys;
use serde_json::Value;
use solana_account_decoder::parse_account_data::ParsedAccount;
use solana_account_decoder::UiAccountData;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_client::rpc_request::TokenAccountsFilter;
use solana_client::rpc_response::RpcKeyedAccount;
use solana_sdk::signer::EncodableKey;
use spl_token::instruction::burn;
use spl_token::state::Mint;
use std::error::Error;
use timed::timed;
use crate::seller_service::load_amm_keys;
use crate::{constants, Provider};
use futures_util::StreamExt;
use indicatif::{ProgressBar, ProgressStyle};
use raydium_library::common;
use reqwest::Client;
use serde_json::json;
use solana_account_decoder::UiAccountEncoding;
use solana_client::rpc_config::RpcAccountInfoConfig;
use solana_client::rpc_config::RpcProgramAccountsConfig;
use solana_client::rpc_filter::Memcmp;
use solana_client::rpc_filter::MemcmpEncodedBytes;
use solana_client::rpc_filter::RpcFilterType;
use solana_sdk::instruction::Instruction;
use solana_sdk::program_pack::Pack;
use solana_sdk::transaction::VersionedTransaction;
use solana_sdk::{
    pubkey::Pubkey, signature::Keypair, signer::Signer,
    transaction::Transaction,
};
use std::fs::File;
use std::io::Write;
use std::path::Path;
#[derive(Debug, Default)]
pub struct Holding {
    pub mint: Pubkey,
    pub ata: Pubkey,
    pub amount: u64,
}
fn parse_holding(ata: RpcKeyedAccount) -> Result<Holding, Box<dyn Error>> {
    if let UiAccountData::Json(ParsedAccount {
        program: _,
        parsed,
        space: _,
    }) = ata.account.data
    {
        let amount = parsed["info"]["tokenAmount"]["amount"]
            .as_str()
            .expect("amount")
            .parse::<u64>()?;
        let mint =
            Pubkey::from_str(parsed["info"]["mint"].as_str().expect("mint"))?;
        let ata = Pubkey::from_str(&ata.pubkey)?;
        Ok(Holding { mint, ata, amount })
    } else {
        Err("failed to parse holding".into())
    }
}
pub async fn sweep_raydium(
    rpc_client: &RpcClient,
    wallet_path: String,
) -> Result<(), Box<dyn Error>> {
    let owner = Keypair::read_from_file(&wallet_path)?.pubkey();
    let atas = rpc_client
        .get_token_accounts_by_owner(
            &owner,
            TokenAccountsFilter::ProgramId(spl_token::id()),
        )
        .await?;
    info!("found {} token accounts", atas.len());
    let holdings = atas
        .iter()
        .map(|ata| parse_holding(ata.clone()).expect("parse holding"))
        .filter(|holding| holding.amount > 0)
        .collect::<Vec<Holding>>();
    let holdings_set = holdings
        .iter()
        .map(|h| h.mint.to_string())
        .collect::<std::collections::HashSet<String>>();
    download_raydium_json(false).await?;
    info!("opening raydium.json and parsing the pools (might take a while)");
    let raw_pools: Value =
        serde_json::from_str(&std::fs::read_to_string("raydium.json")?)?;
    let pools = raw_pools["unOfficial"].as_array().unwrap();
    info!("parsed raydium.json, found {} pools", pools.len());
    let mut pools_map = HashMap::new();
    let mut quote_or_base_map = HashMap::new();
    for pool in pools {
        let base_mint = pool["baseMint"].as_str().unwrap();
        if holdings_set.contains(base_mint) {
            pools_map.insert(base_mint.to_string(), pool);
            quote_or_base_map.insert(base_mint.to_string(), "base");
        }
        let quote_mint = pool["quoteMint"].as_str().unwrap();
        if holdings_set.contains(quote_mint) {
            pools_map.insert(quote_mint.to_string(), pool);
            quote_or_base_map.insert(quote_mint.to_string(), "quote");
        }
    }
    info!(
        "got {} relevant pools, total of {} holdings (ideally shoul equal)",
        pools_map.len(),
        holdings.len()
    );
    for holding in holdings {
        let _frozenlist = ["BEhY5iV6NNGcYnM3miZWAc5jau7Z37foQyQ3BdnyAAGn"];
        info!("{} burning token instead", holding.mint);
        let tx = Transaction::new_signed_with_payer(
            &[burn(
                &spl_token::id(),
                &holding.ata,
                &holding.mint,
                &owner,
                &[&owner],
                holding.amount,
            )?],
            Some(&owner),
            &[&Keypair::read_from_file(&wallet_path)?],
            rpc_client.get_latest_blockhash().await?,
        );
        match rpc_client
            .send_transaction_with_config(
                &tx,
                RpcSendTransactionConfig {
                    encoding: None,
                    skip_preflight: true,
                    preflight_commitment: None,
                    max_retries: None,
                    min_context_slot: None,
                },
            )
            .await
        {
            Ok(signature) => {
                info!("burn transaction: {}", signature);
            }
            Err(e) => {
                warn!("burn transaction failed: {}", e)
            }
        };
        }
    Ok(())
}
pub async fn download_raydium_json(
    update: bool,
) -> Result<(), Box<dyn Error>> {
    if Path::new("raydium.json").exists() && !update {
        warn!("raydium.json already exists. Skipping download.");
        return Ok(());
    }
    let url = "https://api.raydium.io/v2/sdk/liquidity/mainnet.json";
    let client = Client::new();
    let res = client.get(url).send().await?;
    let total_size = res.content_length().unwrap_or(0);
    let pb = ProgressBar::new(total_size);
    pb.set_style(ProgressStyle::default_bar()
        .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {bytes}/{total_bytes} ({eta})")
        .unwrap()
        .progress_chars("#>-"));
    let mut file = File::create("raydium.json")?;
    let mut downloaded: u64 = 0;
    let mut stream = res.bytes_stream();
    while let Some(item) = stream.next().await {
        let chunk = item?;
        file.write_all(&chunk)?;
        let new = std::cmp::min(downloaded + (chunk.len() as u64), total_size);
        downloaded = new;
        pb.set_position(new);
    }
    pb.finish_with_message("Download completed");
    Ok(())
}
pub struct Raydium {}
pub struct SwapArgs {
    pub amm_pool: Pubkey,
    pub input_token_mint: Pubkey,
    pub output_token_mint: Pubkey,
    pub amount: u64,
    pub slippage: u64,
    pub wallet: Keypair,
    pub provider: Provider,
    pub confirmed: bool,
    pub no_sanity: bool,
}
pub struct Swap {
    pre_swap_instructions: Vec<Instruction>,
    post_swap_instructions: Vec<Instruction>,
}
pub struct SwapContext {
    pub amm_program: Pubkey,
    pub amm_pool: Pubkey,
    pub amm_keys: amm::AmmKeys,
    pub market_keys: amm::openbook::MarketPubkeys,
    pub swap: Swap,
    pub user_source: Pubkey,
    pub user_destination: Pubkey,
    pub amount: u64,
    pub input_token_mint: Pubkey,
    pub output_token_mint: Pubkey,
    pub slippage: u64,
    pub swap_base_in: bool,
}
pub async fn get_calc_result(
    rpc_client: &RpcClient,
    amm_pool: &Pubkey,
) -> Result<(amm::CalculateResult, MarketPubkeys, AmmKeys), Box<dyn Error>> {
    let amm_program =
        Pubkey::from_str(constants::RAYDIUM_LIQUIDITY_POOL_V4_PUBKEY)?;
    let amm_keys =
        amm::utils::load_amm_keys(rpc_client, &amm_program, amm_pool).await?;
    debug!("amm keys: {:?}", amm_keys);
    let market_keys = amm::openbook::get_keys_for_market(
        rpc_client,
        &amm_keys.market_program,
        &amm_keys.market,
    )
    .await?;
    debug!("market keys: {:?}", market_keys);
    let result = amm::calculate_pool_vault_amounts(
        rpc_client,
        &amm_program,
        amm_pool,
        &amm_keys,
        &market_keys,
        amm::utils::CalculateMethod::CalculateWithLoadAccount,
    )
    .await?;
    debug!("result: {:?}", result);
    Ok((result, market_keys, amm_keys))
}
pub fn get_burn_pct(
    mint_data: Mint,
    result: amm::CalculateResult,
) -> Result<f64, Box<dyn Error>> {
    let base = 10u64;
    let divisor = base.pow(mint_data.decimals as u32);
    let lp_reserve = result.pool_lp_amount as f64 / divisor as f64;
    let supply = mint_data.supply as f64 / divisor as f64;
    let max_lp_supply = lp_reserve.max(supply);
    let burn_amount = max_lp_supply - supply;
    let burn_pct = if max_lp_supply > 0.0 {
        (burn_amount / max_lp_supply) * 100.0
    } else {
        0.0
    };
    debug!(
        "LP total: {}, LP pooled: {}, LP burnt: {}",
        max_lp_supply, lp_reserve, burn_amount
    );
    Ok(burn_pct)
}
pub fn calc_result_to_financials(
    coin_mint_is_sol: bool,
    result: amm::CalculateResult,
    owner_balance: u64,
) -> f64 {
    let sol_price = 145.;
    if coin_mint_is_sol {
        let sol_amount = result.pool_coin_vault_amount as f64 / 1e9;
        let usd_amount = sol_amount * sol_price;
        let price = result.pool_coin_vault_amount as f64
            / result.pool_pc_vault_amount as f64;
        let owner_balance_sol = owner_balance as f64 * price / 1e9;
        debug!(
            "{}",
            serde_json::to_string_pretty(&json!(
                {
                    "timestamp": chrono::Utc::now().to_rfc3339(),
                    "sol_amount": sol_amount,
                    "usd_amount": usd_amount,
                    "price": price,
                    "owner_balance": owner_balance,
                    "owner_balance_sol": owner_balance_sol,
                }
            ))
            .expect("to string pretty")
        );
        sol_amount
    } else {
        let sol_amount = result.pool_pc_vault_amount as f64 / 1e9;
        let usd_amount = sol_amount * sol_price;
        let price = result.pool_pc_vault_amount as f64
            / result.pool_coin_vault_amount as f64;
        let owner_balance_sol = owner_balance as f64 * price / 1e9;
        debug!(
            "{}",
            serde_json::to_string_pretty(&json!(
                {
                    "timestamp": chrono::Utc::now().to_rfc3339(),
                    "sol_amount": sol_amount,
                    "usd_amount": usd_amount,
                    "price": price,
                    "owner_balance": owner_balance,
                    "owner_balance_sol": owner_balance_sol,
                }
            ))
            .expect("to string pretty")
        );
        sol_amount
    }
}
pub async fn make_swap_context(
    provider: &Provider,
    amm_pool: Pubkey,
    input_token_mint: Pubkey,
    output_token_mint: Pubkey,
    wallet: &Keypair,
    slippage: u64,
    amount: u64,
) -> Result<SwapContext, Box<dyn Error>> {
    let amm_program =
        Pubkey::from_str(constants::RAYDIUM_LIQUIDITY_POOL_V4_PUBKEY)?;
    let amm_keys =
        load_amm_keys(&provider.rpc_client, &amm_program, &amm_pool).await?;
    let market_keys = amm::openbook::get_keys_for_market(
        &provider.rpc_client,
        &amm_keys.market_program,
        &amm_keys.market,
    )
    .await?;
    let mut swap = Swap {
        pre_swap_instructions: vec![],
        post_swap_instructions: vec![],
    };
    let user_source = handle_token_account(
        &mut swap,
        provider,
        &input_token_mint,
        amount,
        &wallet.pubkey(),
        &wallet.pubkey(),
    )
    .await?;
    let user_destination = handle_token_account(
        &mut swap,
        provider,
        &output_token_mint,
        0,
        &wallet.pubkey(),
        &wallet.pubkey(),
    )
    .await?;
    Ok(SwapContext {
        amm_program,
        amm_keys,
        amm_pool,
        market_keys,
        swap,
        user_source,
        user_destination,
        amount,
        input_token_mint,
        output_token_mint,
        slippage,
        swap_base_in: true,
    })
}
#[timed(duration(printer = "info!"))]
pub async fn make_swap_ixs(
    provider: &Provider,
    wallet: &Keypair,
    swap_context: &SwapContext,
    quick: bool,
) -> Result<Vec<Instruction>, Box<dyn Error>> {
    let other_amount_threshold = if !quick {
        let result = raydium_library::amm::calculate_pool_vault_amounts(
            &provider.rpc_client,
            &swap_context.amm_program,
            &swap_context.amm_pool,
            &swap_context.amm_keys,
            &swap_context.market_keys,
            amm::utils::CalculateMethod::Simulate(wallet.pubkey()),
        )
        .await?;
        self::calc_result_to_financials(
            swap_context.market_keys.coin_mint.to_string()
                == constants::SOLANA_PROGRAM_ID,
            result,
            0,
        );
        let direction = if swap_context.input_token_mint
            == swap_context.amm_keys.amm_coin_mint
            && swap_context.output_token_mint
                == swap_context.amm_keys.amm_pc_mint
        {
            amm::utils::SwapDirection::Coin2PC
        } else {
            amm::utils::SwapDirection::PC2Coin
        };
        let other_amount_threshold = amm::swap_with_slippage(
            result.pool_pc_vault_amount,
            result.pool_coin_vault_amount,
            result.swap_fee_numerator,
            result.swap_fee_denominator,
            direction,
            swap_context.amount,
            swap_context.swap_base_in,
            swap_context.slippage,
        )
        .unwrap_or(0);
        let mint_account = provider
            .rpc_client
            .get_account(&swap_context.output_token_mint)
            .await?;
        let mint_data = Mint::unpack(&mint_account.data)?;
        let burn_pct =
            self::get_burn_pct(mint_data, result).expect("get burn pct");
        info!(
            "{}",
            serde_json::to_string_pretty(&json!({
                "pool_pc_vault_amount": result.pool_pc_vault_amount,
                "pool_coin_vault_amount": result.pool_coin_vault_amount,
                "lp_mint": swap_context.amm_keys.amm_lp_mint.to_string(),
                "pool_lp_amount": result.pool_lp_amount,
                "swap_fee_numerator": result.swap_fee_numerator,
                "swap_fee_denominator": result.swap_fee_denominator,
                "other_amount_threshold": other_amount_threshold,
                "liquidity_burn_pct": burn_pct,
            }))?
        );
        if burn_pct < 90. {
            return Err(format!("LP is only {} burnt", burn_pct).into());
        }
        other_amount_threshold
    } else {
        info!("Quick swap, skipping pool vault calculation");
        0
    };
    let swap_ix = amm::instructions::swap(
        &swap_context.amm_program,
        &swap_context.amm_keys,
        &swap_context.market_keys,
        &wallet.pubkey(),
        &swap_context.user_source,
        &swap_context.user_destination,
        swap_context.amount,
        other_amount_threshold,
        swap_context.swap_base_in,
    )?;
    debug!(
        "swap_ix program_id: {:?}, accounts: {} ",
        swap_ix.program_id,
        serde_json::to_string_pretty(
            &swap_ix
                .accounts
                .iter()
                .map(|x| x.pubkey.to_string())
                .collect::<Vec<String>>()
        )?,
    );
    let ixs = [
        make_compute_budget_ixs(0, 300_000),
        swap_context.swap.pre_swap_instructions.clone(),
        vec![swap_ix],
        swap_context.swap.post_swap_instructions.clone(),
    ];
    Ok(ixs.concat())
}
impl Default for Raydium {
    fn default() -> Self {
        Self::new()
    }
}
impl Raydium {
    pub fn new() -> Self {
        Raydium {}
    }
    #[deprecated = "slow and not production required"]
    pub async fn get_amm_pool_id(
        &self,
        provider: &Provider,
        input_mint: &Pubkey,
        output_mint: &Pubkey,
    ) -> Pubkey {
        const INPUT_MINT_OFFSET: usize = 53;
        const OUTPUT_MINT_OFFSET: usize = 85;
        let _accounts = provider
            .rpc_client
            .get_program_accounts_with_config(
                &Pubkey::from_str(constants::OPENBOOK_PROGRAM_ID).unwrap(),
                RpcProgramAccountsConfig {
                    filters: Some(vec![
                        RpcFilterType::Memcmp(Memcmp::new(
                            INPUT_MINT_OFFSET,
                            MemcmpEncodedBytes::Base64(input_mint.to_string()),
                        )),
                        RpcFilterType::Memcmp(Memcmp::new(
                            OUTPUT_MINT_OFFSET,
                            MemcmpEncodedBytes::Base64(output_mint.to_string()),
                        )),
                    ]),
                    account_config: RpcAccountInfoConfig {
                        encoding: Some(UiAccountEncoding::Base64),
                        commitment: Some(
                            solana_sdk::commitment_config::CommitmentConfig::processed(),
                        ),
                        data_slice: None,
                        min_context_slot: None,
                    },
                    ..Default::default()
                },
            )
            .await
            .unwrap();
        Pubkey::default()
    }
    pub fn swap_simple(&self, _output_token_mint: Pubkey, _sol_amount: u64) {
        }
    pub async fn swap(
        &self,
        swap_args: SwapArgs,
    ) -> Result<(), Box<dyn Error>> {
        let SwapArgs {
            amm_pool,
            input_token_mint,
            output_token_mint,
            amount,
            slippage,
            wallet,
            provider,
            confirmed,
            no_sanity,
        } = swap_args;
        let swap_context = self::make_swap_context(
            &provider,
            amm_pool,
            input_token_mint,
            output_token_mint,
            &wallet,
            slippage,
            amount,
        )
        .await?;
        let ixs =
            self::make_swap_ixs(&provider, &wallet, &swap_context, no_sanity)
                .await?;
        info!(
            "{}",
            serde_json::to_string_pretty(&json!({
                "amount": amount,
                "input": input_token_mint.to_string(),
                "output": output_token_mint.to_string(),
                "funder": wallet.pubkey().to_string(),
                "slippage": slippage,
            }))?
        );
        if !confirmed
            && !dialoguer::Confirm::new()
                .with_prompt("Go for it?")
                .interact()?
        {
            return Ok(());
        }
        let tx = Transaction::new_signed_with_payer(
            ixs.as_slice(),
            Some(&wallet.pubkey()),
            &[&wallet],
            provider.rpc_client.get_latest_blockhash().await?,
        );
        let tx = VersionedTransaction::from(tx);
        let sim_res = provider.rpc_client.simulate_transaction(&tx).await?;
        info!("Simulation: {}", serde_json::to_string_pretty(&sim_res)?);
        match provider.send_tx(&tx, true).await {
            Ok(signature) => {
                info!("Transaction {} successful", signature);
                return Ok(());
            }
            Err(e) => {
                error!("Transaction failed: {}", e);
            }
        };
        Ok(())
    }
}
pub async fn handle_token_account(
    swap: &mut Swap,
    provider: &Provider,
    mint: &Pubkey,
    amount: u64,
    owner: &Pubkey,
    funding: &Pubkey,
) -> Result<Pubkey, Box<dyn Error>> {
    if (*mint).to_string() == constants::SOLANA_PROGRAM_ID {
        let rent = provider
            .rpc_client
            .get_minimum_balance_for_rent_exemption(
                spl_token::state::Account::LEN,
            )
            .await?;
        let lamports = rent + amount;
        let seed = &Keypair::new().pubkey().to_string()[0..32];
        let token = generate_pub_key(owner, seed);
        let mut init_ixs =
            create_init_token(&token, seed, mint, owner, funding, lamports);
        let mut close_ixs = common::close_account(&token, owner, owner);
        swap.pre_swap_instructions.append(&mut init_ixs);
        swap.post_swap_instructions.append(&mut close_ixs);
        Ok(token)
    } else {
        let token =
            &spl_associated_token_account::get_associated_token_address(
                owner, mint,
            );
        let mut ata_ixs =
            common::create_ata_token_or_not(funding, mint, owner);
        swap.pre_swap_instructions.append(&mut ata_ixs);
        Ok(*token)
    }
}
pub fn create_init_token(
    token: &Pubkey,
    seed: &str,
    mint: &Pubkey,
    owner: &Pubkey,
    funding: &Pubkey,
    lamports: u64,
) -> Vec<Instruction> {
    vec![
        solana_sdk::system_instruction::create_account_with_seed(
            funding,
            token,
            owner,
            seed,
            lamports,
            spl_token::state::Account::LEN as u64,
            &spl_token::id(),
        ),
        spl_token::instruction::initialize_account(
            &spl_token::id(),
            token,
            mint,
            owner,
        )
        .unwrap(),
    ]
}
pub fn generate_pub_key(from: &Pubkey, seed: &str) -> Pubkey {
    Pubkey::create_with_seed(from, seed, &spl_token::id()).unwrap()
}
pub fn make_compute_budget_ixs(
    price: u64,
    max_units: u32,
) -> Vec<Instruction> {
    vec![
        solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_price(price),
        solana_sdk::compute_budget::ComputeBudgetInstruction::set_compute_unit_limit(max_units),
    ]
}
pub fn make_priority_compute_budget_ixs(
    _provider: &Provider,
    _addressess: &[Pubkey],
) -> Vec<Instruction> {
    vec![]
}