use anchor_client::{
    solana_sdk::{commitment_config::CommitmentConfig, instruction::Instruction},
    Client, Cluster,
};
use anchor_lang::{
    prelude::{AccountMeta, Pubkey},
    system_program,
};
use spl_associated_token_account::get_associated_token_address_with_program_id;
use std::sync::Arc;

use crate::{
    borrow::{
        get_vault_program,
        read::{get_oracle, get_oracle_price_liquidate, VaultOraclePriceLiquidate},
        VaultProgram,
    },
    liquidity::LIQUIDITY_PROGRAM_ID,
    math::{casting::Cast, safe_math::SafeMath, tick::TickMath, u256::safe_multiply_divide},
    programs::vaults::{
        accounts::{Branch, Tick, VaultConfig, VaultMetadata, VaultState},
        client::{
            accounts::{self},
            args,
        },
        types, ID,
    },
    ReadKeypair,
};

use spl_associated_token_account::ID as ASSOCIATED_TOKEN_PROGRAM_ID;

use super::{
    helpers::{
        load_relevant_branches_for_liquidate, load_relevant_ticks_for_liquidate,
        load_relevant_ticks_has_debt_for_liquidate,
    },
    pda,
};
use crate::liquidity;

pub struct LiquidateContext {
    pub accounts: accounts::Liquidate,
    pub other_ixs: Vec<Instruction>,
    pub lookup_table: Pubkey,
    pub remaining_accounts_indices: Vec<u8>,
    pub remaining_accounts: Vec<AccountMeta>,
}

pub struct OtherInstructionsLiquidateParams<'a> {
    pub vault_id: u16,
    pub vault_state: VaultState,
    pub signer: Pubkey,
    pub program: &'a VaultProgram,
}
pub struct OtherInstructionsLiquidate {
    pub other_ixs: Vec<Instruction>,
    pub new_branch_pda: Pubkey,
}

pub(crate) async fn get_other_instructions_liquidate<'a>(
    params: OtherInstructionsLiquidateParams<'a>,
) -> anyhow::Result<OtherInstructionsLiquidate> {
    let program = params.program;

    let mut other_ixs: Vec<Instruction> = vec![];

    let new_branch_id = if params.vault_state.branch_liquidated == 1 {
        params.vault_state.total_branch_id + 1
    } else {
        params.vault_state.current_branch_id
    };

    let new_branch_pda = pda::get_branch(params.vault_id, new_branch_id);
    let tick_pda = pda::get_tick(params.vault_id, params.vault_state.topmost_tick);

    let (new_branch_data, tick_data) = tokio::join!(
        async { program.account::<Branch>(new_branch_pda).await.ok() },
        async { program.account::<Tick>(tick_pda).await.ok() }
    );

    if new_branch_data.is_none() {
        let ix = program
            .request()
            .accounts(accounts::InitBranch {
                signer: params.signer,
                vault_config: pda::get_vault_config(params.vault_id),
                branch: new_branch_pda,
                system_program: system_program::ID,
            })
            .args(args::InitBranch {
                vault_id: params.vault_id,
                branch_id: new_branch_id,
            })
            .instructions()?
            .remove(0);

        other_ixs.push(ix);
    }

    if tick_data.is_none() {
        let ix = program
            .request()
            .accounts(accounts::InitTick {
                signer: params.signer,
                vault_config: pda::get_vault_config(params.vault_id),
                tick_data: tick_pda,
                system_program: system_program::ID,
            })
            .args(args::InitTick {
                vault_id: params.vault_id,
                tick: params.vault_state.topmost_tick,
            })
            .instructions()?
            .remove(0);

        other_ixs.push(ix);
    }

    return Ok(OtherInstructionsLiquidate {
        other_ixs,
        new_branch_pda,
    });
}

pub struct RemainingAccountsLiquidateParams<'a> {
    pub vault_id: u16,
    pub vault_state: VaultState,
    pub vault_config: VaultConfig,
    pub other_ixs: Vec<Instruction>,
    pub oracle_price: Option<u64>,
    pub program: &'a VaultProgram,
    pub signer: Pubkey,
}
pub struct RemainingAccountsLiquidate {
    pub other_ixs: Vec<Instruction>,
    pub remaining_accounts: Vec<AccountMeta>,
    pub remaining_accounts_indices: Vec<u8>,
}

pub(crate) async fn get_remaining_accounts_liquidate<'a>(
    params: RemainingAccountsLiquidateParams<'a>,
) -> anyhow::Result<RemainingAccountsLiquidate> {
    let program = params.program;

    let mut remaining_accounts = vec![];
    let mut other_ixs: Vec<Instruction> = vec![];

    let (oracle, branches) = tokio::join!(
        async {
            if let Some(price) = params.oracle_price {
                let oracle_data = get_oracle(params.vault_config.oracle, &program).await?;
                Ok(VaultOraclePriceLiquidate {
                    price: price.cast()?,
                    sources: oracle_data.sources,
                })
            } else {
                get_oracle_price_liquidate(params.vault_config.oracle, &program).await
            }
        },
        load_relevant_branches_for_liquidate(params.vault_id, params.vault_state, &program)
    );
    let oracle = oracle?;
    let branches = branches?;

    for source in &oracle.sources {
        remaining_accounts.push(AccountMeta::new_readonly(source.source, false));
    }
    let liquidation_ratio: u128 = safe_multiply_divide(
        oracle.price,
        TickMath::ZERO_TICK_SCALED_RATIO,
        10u128.pow(15),
    )?;

    let threshold_ratio: u128 = liquidation_ratio
        .safe_mul(
            params
                .vault_config
                .liquidation_threshold
                .cast()
                .map_err(|_| anyhow::anyhow!("Failed to cast liquidation threshold"))?,
        )
        .map_err(|err| anyhow::anyhow!("Failed to calculate threshold ratio: {}", err))?
        .safe_div(1000)
        .map_err(|err| anyhow::anyhow!("Failed to calculate threshold ratio: {}", err))?;

    for branch in &branches {
        remaining_accounts.push(AccountMeta::new(
            pda::get_branch(params.vault_id, branch.branch_id),
            false,
        ))
    }
    let (liquidation_tick, _) = TickMath::get_tick_at_ratio(threshold_ratio)?;

    let (tick_accounts, next_tick) = load_relevant_ticks_for_liquidate(
        params.vault_id,
        params.vault_state,
        liquidation_tick,
        &program,
    )
    .await?;

    let tick_has_debt = load_relevant_ticks_has_debt_for_liquidate(
        params.vault_id,
        params.vault_state.topmost_tick,
        next_tick,
        &program,
    )
    .await?;

    let tick_futures: Vec<_> = tick_accounts
        .iter()
        .map(async |tick_account| {
            let tick_pda = pda::get_tick(tick_account.vault_id, tick_account.tick);
            if let Err(_) = program.account::<Tick>(tick_pda).await {
                return Some(
                    program
                        .request()
                        .args(args::InitTick {
                            vault_id: tick_account.vault_id,
                            tick: tick_account.tick,
                        })
                        .accounts(accounts::InitTick {
                            signer: params.signer,
                            vault_config: pda::get_vault_config(tick_account.vault_id),
                            tick_data: tick_pda,
                            system_program: system_program::ID,
                        })
                        .instructions()
                        .unwrap()
                        .remove(0),
                );
            }
            None
        })
        .collect();

    let init_instructions: Vec<Instruction> = futures::future::join_all(tick_futures)
        .await
        .into_iter()
        .flatten()
        .collect();

    for init_instruction in init_instructions {
        other_ixs.push(init_instruction);
    }

    for tick_account in &tick_accounts {
        remaining_accounts.push(AccountMeta::new(
            pda::get_tick(tick_account.vault_id, tick_account.tick),
            false,
        ));
    }

    for tick_has_debt_array in &tick_has_debt {
        remaining_accounts.push(AccountMeta::new(
            pda::get_tick_has_debt(params.vault_id, tick_has_debt_array.index),
            false,
        ));
    }

    return Ok(RemainingAccountsLiquidate {
        other_ixs,
        remaining_accounts,
        remaining_accounts_indices: vec![
            oracle.sources.len() as u8,
            branches.len() as u8,
            tick_accounts.len() as u8,
            tick_has_debt.len() as u8,
        ],
    });
}

async fn get_liquidate_ctx(
    vault_id: u16,
    to: Pubkey,
    signer: Pubkey,
    cluster: Cluster,
) -> anyhow::Result<LiquidateContext> {
    let program = get_vault_program(
        cluster,
        Arc::new(ReadKeypair::new()),
        CommitmentConfig::confirmed(),
    )?;
    let (vault_state, vault_config, vault_metadata) = tokio::join!(
        program.account::<VaultState>(pda::get_vault_state(vault_id)),
        program.account::<VaultConfig>(pda::get_vault_config(vault_id)),
        program.account::<VaultMetadata>(pda::get_vault_metadata(vault_id))
    );

    let vault_state = vault_state?;
    let vault_config = vault_config?;
    let vault_metadata = vault_metadata?;

    let rpc = program.rpc();
    let (supply_token_program_acc, borrow_token_program_acc) = tokio::join!(
        rpc.get_account(&vault_config.supply_token),
        rpc.get_account(&vault_config.borrow_token)
    );

    let (supply_token_program, borrow_token_program) = (
        supply_token_program_acc?.owner,
        borrow_token_program_acc?.owner,
    );

    let OtherInstructionsLiquidate {
        other_ixs,
        new_branch_pda,
    } = get_other_instructions_liquidate(OtherInstructionsLiquidateParams {
        vault_id,
        vault_state,
        program: &program,
        signer,
    })
    .await?;

    let RemainingAccountsLiquidate {
        other_ixs,
        remaining_accounts,
        remaining_accounts_indices,
    } = get_remaining_accounts_liquidate(RemainingAccountsLiquidateParams {
        other_ixs,
        vault_id,
        vault_state,
        vault_config,
        oracle_price: None,
        program: &program,
        signer,
    })
    .await?;

    return Ok(LiquidateContext {
        accounts: accounts::Liquidate {
            signer,
            signer_token_account: get_associated_token_address_with_program_id(
                &signer,
                &vault_config.borrow_token,
                &borrow_token_program,
            ),
            to,
            to_token_account: get_associated_token_address_with_program_id(
                &to,
                &vault_config.supply_token,
                &supply_token_program,
            ),
            vault_config: pda::get_vault_config(vault_id),
            vault_state: pda::get_vault_state(vault_id),
            supply_token: vault_config.supply_token,
            borrow_token: vault_config.borrow_token,
            oracle: vault_config.oracle,
            new_branch: new_branch_pda,
            supply_token_reserves_liquidity: liquidity::pda::get_token_reserve(
                vault_config.supply_token,
            ),
            borrow_token_reserves_liquidity: liquidity::pda::get_token_reserve(
                vault_config.borrow_token,
            ),
            vault_supply_position_on_liquidity: liquidity::pda::get_user_supply_position(
                vault_config.supply_token,
                pda::get_vault_config(vault_id),
            ),
            vault_borrow_position_on_liquidity: liquidity::pda::get_user_borrow_position(
                vault_config.borrow_token,
                pda::get_vault_config(vault_id),
            ),
            supply_rate_model: liquidity::pda::get_rate_model(vault_config.supply_token),
            borrow_rate_model: liquidity::pda::get_rate_model(vault_config.borrow_token),
            supply_token_claim_account: Some(liquidity::pda::get_user_claim_account(
                vault_config.supply_token,
                pda::get_vault_config(vault_id),
            )),
            liquidity: liquidity::pda::get_liquidity(),
            liquidity_program: LIQUIDITY_PROGRAM_ID,
            vault_supply_token_account: get_associated_token_address_with_program_id(
                &liquidity::pda::get_liquidity(),
                &vault_config.supply_token,
                &supply_token_program,
            ),
            vault_borrow_token_account: get_associated_token_address_with_program_id(
                &liquidity::pda::get_liquidity(),
                &vault_config.borrow_token,
                &borrow_token_program,
            ),
            supply_token_program,
            borrow_token_program,
            system_program: system_program::ID,
            associated_token_program: ASSOCIATED_TOKEN_PROGRAM_ID,
            oracle_program: vault_config.oracle_program,
        },
        other_ixs,
        lookup_table: vault_metadata.lookup_table,
        remaining_accounts_indices,
        remaining_accounts,
    });
}

#[derive(Debug, Clone)]
pub struct Liquidate {
    pub ixs: Vec<Instruction>,
    pub address_lookup_table_addresses: Vec<Pubkey>,
}

pub async fn get_liquidate_ix(
    vault_id: u16,
    debt_amt: u64,
    col_per_unit_debt: u128,
    absorb: bool,
    to: Pubkey,
    signer: Pubkey,
    cluster: Cluster,
) -> anyhow::Result<Liquidate> {
    let provider = Client::new_with_options(
        cluster.clone(),
        Arc::new(ReadKeypair::new()),
        CommitmentConfig::confirmed(),
    );

    let program = provider.program(ID)?;

    let ctx = get_liquidate_ctx(vault_id, to, signer, cluster).await?;

    let mut ixs = ctx.other_ixs.clone();

    let ix = program
        .request()
        .accounts(ctx.accounts)
        .accounts(ctx.remaining_accounts)
        .args(args::Liquidate {
            debt_amt: debt_amt,
            col_per_unit_debt: col_per_unit_debt,
            absorb: absorb,
            transfer_type: Some(types::TransferType::DIRECT),
            remaining_accounts_indices: ctx.remaining_accounts_indices,
        })
        .instructions()?
        .remove(0);

    ixs.push(ix);

    Ok(Liquidate {
        ixs,
        address_lookup_table_addresses: vec![ctx.lookup_table],
    })
}
