use super::{pda, FLOASHLOAN_PROGRAM_ID};
use crate::{
    liquidity::{self, LIQUIDITY_PROGRAM_ID},
    math::{casting::Cast, safe_math::SafeMath},
    programs::flashloan::{
        accounts::FlashloanAdmin,
        client::{accounts, args},
    },
    ReadKeypair,
};
use anchor_client::{
    solana_sdk::{commitment_config::CommitmentConfig, instruction::Instruction},
    Client, Cluster,
};
use anchor_lang::{prelude::Pubkey, solana_program, system_program};
use spl_associated_token_account::{
    get_associated_token_address_with_program_id, ID as ASSOCIATED_TOKEN_PROGRAM_ID,
};
use std::sync::Arc;

#[derive(Debug, Clone)]
pub struct Flashloan {
    pub borrow_ix: Instruction,
    pub payback_ix: Instruction,
}

pub async fn get_flashloan_ix(
    amount: u64,
    asset: Pubkey,
    signer: Pubkey,
    cluster: Cluster,
) -> anyhow::Result<Flashloan> {
    let provider = Client::new_with_options(
        cluster.clone(),
        Arc::new(ReadKeypair::new()),
        CommitmentConfig::confirmed(),
    );
    let program = provider.program(FLOASHLOAN_PROGRAM_ID)?;

    let token_program = {
        let account = program.rpc().get_account(&asset).await?;

        account.owner
    };

    let flashloan_admin: FlashloanAdmin = program.account(pda::get_flashloan_admin()).await?;

    let borrow_ix = program
        .request()
        .accounts(accounts::FlashloanBorrow {
            signer: signer,
            flashloan_admin: pda::get_flashloan_admin(),
            signer_borrow_token_account: get_associated_token_address_with_program_id(
                &signer,
                &asset,
                &token_program,
            ),
            mint: asset,
            flashloan_token_reserves_liquidity: liquidity::pda::get_token_reserve(asset),
            flashloan_borrow_position_on_liquidity: liquidity::pda::get_user_borrow_position(
                asset,
                pda::get_flashloan_admin(),
            ),
            rate_model: liquidity::pda::get_rate_model(asset),
            vault: get_associated_token_address_with_program_id(
                &liquidity::pda::get_liquidity(),
                &asset,
                &token_program,
            ),
            liquidity: liquidity::pda::get_liquidity(),
            liquidity_program: LIQUIDITY_PROGRAM_ID,
            token_program,
            associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
            system_program: system_program::ID,
            instruction_sysvar: solana_program::sysvar::instructions::ID,
        })
        .args(args::FlashloanBorrow { amount: amount })
        .instructions()?
        .remove(0);

    let flashloan_fee: u128 = flashloan_admin
        .flashloan_fee
        .cast()
        .map_err(|err| anyhow::Error::msg(err.to_string()))?;

    let flashloan_fee_amount = amount
        .cast::<u128>()
        .map_err(|err| anyhow::Error::msg(err.to_string()))?
        .safe_mul(flashloan_fee)
        .map_err(|err| anyhow::Error::msg(err.to_string()))?
        .safe_div_ceil(10000)
        .map_err(|err| anyhow::Error::msg(err.to_string()))?
        .cast::<u64>()
        .map_err(|err| anyhow::Error::msg(err.to_string()))?;

    let payback_amount = amount
        .safe_add(flashloan_fee_amount)
        .map_err(|err| anyhow::Error::msg(err.to_string()))?;

    let payback_ix = program
        .request()
        .accounts(accounts::FlashloanPayback {
            signer: signer,
            flashloan_admin: pda::get_flashloan_admin(),
            signer_borrow_token_account: get_associated_token_address_with_program_id(
                &signer,
                &asset,
                &token_program,
            ),
            mint: asset,
            flashloan_token_reserves_liquidity: liquidity::pda::get_token_reserve(asset),
            flashloan_borrow_position_on_liquidity: liquidity::pda::get_user_borrow_position(
                asset,
                pda::get_flashloan_admin(),
            ),
            rate_model: liquidity::pda::get_rate_model(asset),
            vault: get_associated_token_address_with_program_id(
                &liquidity::pda::get_liquidity(),
                &asset,
                &token_program,
            ),
            liquidity: liquidity::pda::get_liquidity(),
            liquidity_program: LIQUIDITY_PROGRAM_ID,
            token_program,
            associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
            system_program: system_program::ID,
            instruction_sysvar: solana_program::sysvar::instructions::ID,
        })
        .args(args::FlashloanPayback {
            amount: payback_amount,
        })
        .instructions()?
        .remove(0);

    Ok(Flashloan {
        borrow_ix,
        payback_ix,
    })
}
