use std::{str::FromStr, sync::Arc};

use super::{
    constants::*,
    helpers::{end_liquidate, get_branches_from_remaining_accounts},
    tick_has_debt::{
        fetch_next_tick_absorb, fetch_next_tick_liquidate,
        get_tick_has_debt_from_remaining_accounts_liquidate,
    },
};
use anchor_client::{
    solana_client::rpc_config::RpcSimulateTransactionConfig,
    solana_sdk::{commitment_config::CommitmentConfig, transaction::Transaction},
    Cluster,
};
use anchor_client::{solana_sdk::message::Message, Client};

use anchor_lang::prelude::{AccountMeta, Pubkey};
use anyhow::Ok;

use crate::{
    borrow::{
        errors::ErrorCodes,
        instructions::{
            get_other_instructions_liquidate, get_remaining_accounts_liquidate,
            OtherInstructionsLiquidate, OtherInstructionsLiquidateParams,
            RemainingAccountsLiquidate, RemainingAccountsLiquidateParams,
        },
        state::{BranchMemoryVars, CurrentLiquidity, TickMemoryVars},
        ticks::{get_current_partials_ratio, get_next_ref_tick, get_ticks_from_remaining_accounts},
        VaultProgram,
    },
    liquidity,
    math::{
        bn::div_big_number, casting::Cast, safe_math::SafeMath, tick::TickMath,
        u256::safe_multiply_divide,
    },
    oracle::ORACLE_PROGRAM_ID,
    programs::{
        oracle,
        vaults::{
            self,
            accounts::{
                Branch, Tick, TickHasDebtArray, TokenReserve, VaultConfig, VaultMetadata,
                VaultState,
            },
            types::Sources,
        },
    },
    ReadKeypair,
};

use super::{get_vault_program, pda, simulations::VaultLiquidation};

pub async fn get_all_vault_config(cluster: Cluster) -> anyhow::Result<Vec<VaultConfig>> {
    let program = get_vault_program(
        cluster,
        Arc::new(ReadKeypair::new()),
        CommitmentConfig::confirmed(),
    )?;

    Ok(program
        .accounts::<VaultConfig>(vec![])
        .await?
        .into_iter()
        .map(|(_, config)| config)
        .collect::<Vec<_>>())
}

pub async fn get_vault_config(vault_id: u16, cluster: Cluster) -> anyhow::Result<VaultConfig> {
    let program = get_vault_program(
        cluster,
        Arc::new(ReadKeypair::new()),
        CommitmentConfig::confirmed(),
    )?;

    Ok(program.account(pda::get_vault_config(vault_id)).await?)
}

pub async fn get_vault_state(vault_id: u16, cluster: Cluster) -> anyhow::Result<VaultState> {
    let program = get_vault_program(
        cluster,
        Arc::new(ReadKeypair::new()),
        CommitmentConfig::confirmed(),
    )?;

    Ok(program.account(pda::get_vault_state(vault_id)).await?)
}

pub async fn get_vault_metadata(vault_id: u16, cluster: Cluster) -> anyhow::Result<VaultState> {
    let program = get_vault_program(
        cluster,
        Arc::new(ReadKeypair::new()),
        CommitmentConfig::confirmed(),
    )?;

    Ok(program.account(pda::get_vault_state(vault_id)).await?)
}

pub async fn get_vault_liquidations(
    vault_id: u16,
    col_per_unit_debt: u128,
    absorb: bool,
    signer: Option<Pubkey>,
    cluster: Cluster,
) -> anyhow::Result<Vec<VaultLiquidation>> {
    get_vault_liquidations_with_options(
        vault_id,
        VaultLiquidationsOptions {
            signer,
            oracle_price: None,
            col_per_unit_debt,
            absorb,
            cluster,
        },
    )
    .await
}

pub struct VaultLiquidationsOptions {
    pub signer: Option<Pubkey>,
    pub oracle_price: Option<u64>,
    pub col_per_unit_debt: u128,
    pub absorb: bool,
    pub cluster: Cluster,
}

impl Default for VaultLiquidationsOptions {
    fn default() -> Self {
        Self {
            signer: None,
            oracle_price: None,
            col_per_unit_debt: 0,
            absorb: false,
            cluster: Cluster::Mainnet,
        }
    }
}

pub async fn get_vault_liquidations_with_options(
    vault_id: u16,
    options: VaultLiquidationsOptions,
) -> anyhow::Result<Vec<VaultLiquidation>> {
    let signer = options.signer.unwrap_or(Pubkey::from_str(
        "HEyJLdMfZhhQ7FHCtjD5DWDFNFQhaeAVAsHeWqoY6dSD",
    )?);

    let debt_amt = (2_u128.pow(64) - 1) as u64;

    let program = get_vault_program(
        options.cluster.clone(),
        Arc::new(ReadKeypair::from_pubkey(signer)),
        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 OtherInstructionsLiquidate {
        other_ixs,
        new_branch_pda,
        ..
    } = get_other_instructions_liquidate(OtherInstructionsLiquidateParams {
        vault_id,
        vault_state,
        program: &program,
        signer,
    })
    .await?;

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

    let (_, _, actual_debt_amt, actual_col_amt) = {
        let (vault_state, vault_config, new_branch) = tokio::join!(
            program.account::<VaultState>(pda::get_vault_state(vault_id)),
            program.account::<VaultConfig>(pda::get_vault_config(vault_id)),
            program.account::<Branch>(new_branch_pda)
        );
        let mut vault_state = vault_state?;
        let vault_config = vault_config?;
        let mut new_branch = new_branch?;

        // Check for valid input
        if vault_state.topmost_tick == TickMath::COLD_TICK {
            return Ok(vec![]);
        }

        let debt_amt = scale_amounts(debt_amt.cast()?, vault_metadata.borrow_mint_decimals)?;
        let (supply_token_reserves, borrow_token_reserves) = tokio::join!(
            program.account::<TokenReserve>(liquidity::pda::get_token_reserve(
                vault_config.supply_token
            )),
            program.account::<TokenReserve>(liquidity::pda::get_token_reserve(
                vault_config.borrow_token
            ))
        );
        let supply_token_reserves = supply_token_reserves?;
        let borrow_token_reserves = borrow_token_reserves?;

        // Below are exchange prices of vaults
        let (
            _,               // liquidity_supply_ex_price
            _,               // liquidity_borrow_ex_price
            supply_ex_price, // vault_supply_ex_price
            borrow_ex_price, // vault_borrow_ex_price
        ) = vault_state.load_exchange_prices(
            &vault_config,
            &supply_token_reserves,
            &borrow_token_reserves,
        )?;

        let mut current_data: Box<CurrentLiquidity> = Box::new(CurrentLiquidity::default());

        let exchange_rate: u128 = if let Some(price) = options.oracle_price {
            price.cast()?
        } else {
            get_oracle_price_liquidate_from_remaining_accounts(
                vault_config.oracle,
                &remaining_accounts,
                &remaining_accounts_indices,
                &program,
            )
            .await?
            .price
        };

        // @dev setup tick in memory vars for liquidation
        let (col_per_debt, liquidation_tick, max_tick) = get_ticks_from_oracle_price(
            &vault_config,
            supply_ex_price,
            borrow_ex_price,
            &remaining_accounts,
            &remaining_accounts_indices,
            exchange_rate,
        )
        .await?;

        let (tick_has_debts, branches, ticks) = tokio::join!(
            get_tick_has_debt_from_remaining_accounts_liquidate(
                &remaining_accounts,
                &remaining_accounts_indices,
                &program,
            ),
            get_branches_from_remaining_accounts(
                &remaining_accounts,
                &remaining_accounts_indices,
                &program,
            ),
            get_ticks_from_remaining_accounts(
                &remaining_accounts,
                &remaining_accounts_indices,
                &program,
            )
        );

        let mut tick_has_debts = tick_has_debts?;
        let mut branches = branches?;
        let mut ticks = ticks?;

        // Check if tick is above max limit, absorb it first
        if vault_state.topmost_tick > max_tick {
            // Call absorb function to handle bad debt above max limit
            // @dev passing vault_state as a mut reference, that means it will be updated inside the absorb function
            self::absorb(
                &mut vault_state,
                &mut ticks,
                &mut tick_has_debts,
                &mut branches,
                &mut new_branch,
                max_tick,
            )?;

            if debt_amt == 0 {
                // If debt_amt was 0, we just wanted to absorb
                return Ok(vec![]);
            }
        }

        // Get current tick from vault state
        current_data.tick = vault_state.topmost_tick;

        if debt_amt
            < scale_amounts(
                MIN_DEBT.cast::<i128>()?,
                vault_metadata.borrow_mint_decimals,
            )?
        {
            return Err((ErrorCodes::VaultInvalidLiquidationAmt).into());
        }

        current_data.tick_status = vault_state.get_tick_status();

        let mut tick_info: Box<TickMemoryVars> = Box::new(TickMemoryVars::default());
        tick_info.tick = current_data.tick;

        // Calculate debt remaining to liquidate
        // debtAmt_ should be less than 2**128 & EXCHANGE_PRICES_PRECISION is 1e12
        current_data.debt_remaining = debt_amt
            .cast::<u128>()?
            .safe_mul(EXCHANGE_PRICES_PRECISION)?
            .safe_div(borrow_ex_price)?;

        // Get total debt for minimum check
        let total_debt: u128 = vault_state.get_total_borrow()?;

        if total_debt.safe_div(BILLION)? > current_data.debt_remaining {
            // if liquidation amount is less than 1e9 of total debt then revert
            // so if total debt is $1B then minimum liquidation limit = $1
            // so if total debt is $1T then minimum liquidation limit = $1000
            // partials precision is slightly above 1e9 so this will make sure that on every liquidation at least 1 partial gets liquidated
            // not sure if it can result in any issue but restricting amount further more to remove very low amount scenarios totally
            return Err((ErrorCodes::VaultInvalidLiquidationAmt).into());
        }

        // Handle absorbed liquidity first if requested
        if options.absorb {
            vault_state.absorb_dust_amount_for_liquidate(&mut current_data)?;
        }

        // current tick should be greater than liquidationTick and it cannot be greater than maxTick as absorb will run
        if current_data.tick > liquidation_tick && current_data.debt_remaining > 0 {
            // branch related stuffs
            let mut branch: Box<BranchMemoryVars> = Box::new(BranchMemoryVars::default());

            {
                // @dev current branch should exist in the branch accounts
                let branch_0 = branches
                    .iter()
                    .find(|b| b.branch_id == vault_state.current_branch_id)
                    .ok_or(ErrorCodes::VaultBranchNotFound)?;

                branch.set_branch_data_in_memory(&branch_0)?;
            }

            let mut next_tick = TickMath::COLD_TICK;

            if current_data.is_perfect_tick() {
                // top tick is not liquidated. Hence it's a perfect tick.
                current_data.ratio = TickMath::get_ratio_at_tick(tick_info.tick)?;
                // if current tick in liquidation is a perfect tick then it is also the next tick that has debt.
                next_tick = current_data.tick;
            } else {
                (current_data.ratio, tick_info.partials) = get_current_partials_ratio(
                    branch.data.minima_tick_partials,
                    TickMath::get_ratio_at_tick(tick_info.tick)?,
                )?;

                // Check for edge case: liquidation tick+1 == current tick and partials == 1
                // This means there's nothing to liquidate anymore
                if liquidation_tick + 1 == tick_info.tick && tick_info.partials == 1 {
                    return Ok(vec![]);
                }
            }

            let mut is_first_iteration = true;
            // Main liquidation loop
            loop {
                let additional_debt: u128 = if current_data.is_perfect_tick() {
                    // not liquidated -> Getting the debt from tick data itself
                    // Updating tick on storage with removing debt & adding connection to branch

                    let tick_data = ticks
                        .iter_mut()
                        .find(|b| b.tick == current_data.tick)
                        .ok_or(ErrorCodes::VaultTickNotFound)?;

                    let debt = tick_data.get_raw_debt()?;
                    tick_data.set_liquidated(branch.id, branch.debt_factor);

                    debt
                } else {
                    // Tick is already liquidated - Get debt from branch data
                    branch.data.debt_liquidity.cast()?
                    // debt in branch
                };

                // Adding new debt into active debt for liquidation
                current_data.debt = current_data.debt.safe_add(additional_debt)?;

                // Adding new col into active col for liquidation
                // Ratio is in 2**48 decimals hence multiplying debt with 2**48 to get proper collateral
                current_data.col = current_data.col.safe_add(
                    additional_debt
                        .safe_mul(TickMath::ZERO_TICK_SCALED_RATIO)?
                        .safe_div(current_data.ratio)?,
                )?;

                // Find next tick with debt or check if we reach liquidation threshold
                if (next_tick == current_data.tick && current_data.is_perfect_tick())
                    || is_first_iteration
                {
                    is_first_iteration = false;

                    next_tick = fetch_next_tick_liquidate(
                        &mut tick_has_debts,
                        current_data.tick,
                        liquidation_tick,
                        current_data.is_perfect_tick(), // in 1st loop tickStatus can be 2. Meaning not a perfect current tick
                    )?;
                }

                (current_data.ref_tick, current_data.ref_tick_status) =
                    get_next_ref_tick(branch.minima_tick, next_tick, liquidation_tick)?;

                if current_data.is_ref_tick_liquidated() {
                    // Merge current branch with base branch
                    // Fetching base branch data to get the base branch's partial
                    let base_branch_id = branch.data.connected_branch_id;

                    // Find the corresponding branch in our list
                    let base_branch = branches
                        .iter()
                        .find(|b| b.branch_id == base_branch_id)
                        .ok_or(ErrorCodes::VaultBranchNotFound)?;
                    branch.set_base_branch_data(&base_branch)?;

                    (current_data.ref_ratio, tick_info.partials) = get_current_partials_ratio(
                        base_branch.minima_tick_partials,
                        TickMath::get_ratio_at_tick(current_data.ref_tick)?,
                    )?;
                } else {
                    // refTickStatus can only be 1 (next tick from perfect tick) or 3 (liquidation threshold tick)
                    current_data.ref_ratio = TickMath::get_ratio_at_tick(current_data.ref_tick)?;
                    tick_info.partials = X30;
                }

                // Formula: (debt_ - x) / (col_ - (x * colPerDebt_)) = ratioEnd_
                // x = ((ratioEnd_ * col) - debt_) / ((colPerDebt_ * ratioEnd_) - 1)
                // x is debt_liquidated
                // col_ = debt_ / ratioStart_ -> (current_data.debt / current_data.ratio)
                // ratioEnd_ is current_data.ref_ratio
                //
                // Calculation results of numerator & denominator is always negative
                // which will cancel out to give positive output in the end so we can safely cast to u128.
                // For numerator:
                // ratioStart can only be >= ratioEnd so first part can only be reducing current_data.debt leading to
                // current_data.debt reduced - current_data.debt original * 1e27 -> can only be a negative number
                // For denominator:
                // col_per_debt and current_data.ref_ratio are inversely proportional to each other.
                // The maximum value they can ever be is ~9.97e26 which is the 0.3% away from 100% because liquidation
                // threshold + liquidation penalty can never be > 99.7%. This can also be verified by going back from
                // min / max ratio values further up where we fetch oracle price etc.
                // As optimization we can inverse numerator and denominator subtraction to directly get a positive number.

                // Calculate debt to liquidate
                let mut debt_liquidated: u128 = current_data.get_debt_liquidated(col_per_debt)?;

                // Calculate collateral to liquidate
                // extremely unlikely to overflow considering debt_liquidated is realistically within u64.
                let mut col_liquidated: u128 = debt_liquidated
                    .safe_mul(col_per_debt)?
                    .safe_div(10u128.pow(RATE_OUTPUT_DECIMALS))?;

                // Adjust for edge case
                if current_data.debt == debt_liquidated {
                    debt_liquidated = debt_liquidated.safe_sub(1)?;
                }

                if debt_liquidated >= current_data.debt_remaining
                    || current_data.is_ref_tick_liquidation_threshold()
                {
                    // @dev tick_has_debt_data already updated in fetch_next_tick_liquidate

                    // End of liquidation as full amount to liquidate or liquidation threshold tick has been reached
                    end_liquidate(
                        &mut current_data,
                        &mut tick_info,
                        &mut branch,
                        &mut debt_liquidated,
                        &mut col_liquidated,
                        col_per_debt,
                        scale_amounts(
                            MINIMUM_BRANCH_DEBT.cast::<i128>()?,
                            vault_metadata.borrow_mint_decimals,
                        )?
                        .cast::<u128>()?,
                    )?;

                    let branch_to_update = branches
                        .iter_mut()
                        .find(|b| b.branch_id == branch.id)
                        .ok_or(ErrorCodes::VaultBranchNotFound)?;

                    branch_to_update.update_state_at_liq_end(
                        tick_info.tick,
                        tick_info.partials,
                        current_data.debt,
                        branch.debt_factor.cast()?,
                    )?;

                    vault_state.update_state_at_liq_end(tick_info.tick, branch.id)?;

                    break;
                }

                // Calculate new debt factor
                // debtFactor = debtFactor * (liquidatableDebt - debtLiquidated) / liquidatableDebt
                // -> debtFactor * leftOverDebt / liquidatableDebt
                let debt_factor = current_data.get_debt_factor(debt_liquidated)?;

                current_data.reduce_debt_remaining(debt_liquidated)?;

                // Update totals
                current_data.update_totals(debt_liquidated, col_liquidated)?;

                // Update branch debt factor using mulDivBigNumber equivalent
                branch.update_branch_debt_factor(debt_factor)?;

                if current_data.is_ref_tick_liquidated() {
                    // Ref tick is base branch's minima, so we need to merge the current branch to base branch
                    // and make base branch the current branch

                    let new_branch_debt_factor = branch.base_branch_data.debt_factor;

                    let current_branch = branches
                        .iter_mut()
                        .find(|b| b.branch_id == branch.id)
                        .ok_or(ErrorCodes::VaultBranchNotFound)?;

                    current_branch.merge_with_base_branch(div_big_number(
                        new_branch_debt_factor,
                        branch.debt_factor,
                    )?)?;

                    branch.debt_factor = new_branch_debt_factor;

                    // Update branch variables to use base branch now
                    branch.update_branch_to_base_branch();
                }

                // Make reference tick the current tick for next iteration
                current_data.update_next_iterations_with_ref();
            }
        }

        // Calculate net token amounts using exchange price
        let (actual_debt_amt, actual_col_amt) =
            current_data.get_actual_amounts(borrow_ex_price, supply_ex_price, debt_amt.cast()?)?;

        // Check if slippage tolerance is maintained
        if actual_col_amt
            .safe_mul(10u128.pow(RATE_OUTPUT_DECIMALS))?
            .safe_div(actual_debt_amt)?
            < options.col_per_unit_debt
        {
            return Err((ErrorCodes::VaultExcessSlippageLiquidation).into());
        }

        vault_state.reduce_total_supply(current_data.total_col_liq)?;
        vault_state.reduce_total_borrow(current_data.total_debt_liq)?;

        (
            vault_config.vault_id,
            vault_config.bump,
            unscale_amounts(actual_debt_amt.cast()?, vault_metadata.borrow_mint_decimals)?
                .cast::<u128>()?, // return unscaled amount
            unscale_amounts(actual_col_amt.cast()?, vault_metadata.supply_mint_decimals)?
                .cast::<u128>()?, // return unscaled amount
        )
    };

    let topmost_tick = vault_state.topmost_tick;

    Ok(vec![VaultLiquidation {
        amt_in: actual_debt_amt.cast()?,
        amt_out: actual_col_amt.cast()?,
        top_tick: topmost_tick,
    }])
}

pub fn scale_amounts(amount: i128, decimals: u8) -> anyhow::Result<i128> {
    let scale: u128 = if decimals <= 9 {
        10u128.pow((9 - decimals).cast()?)
    } else {
        return Err((ErrorCodes::VaultInvalidDecimals).into());
    };

    amount.safe_mul(scale.cast()?)
}

pub fn unscale_amounts(amount: i128, decimals: u8) -> anyhow::Result<i128> {
    let scale: u128 = if decimals <= 9 {
        10u128.pow((9 - decimals).cast()?)
    } else {
        return Err((ErrorCodes::VaultInvalidDecimals).into());
    };

    amount.safe_div(scale.cast()?)
}

fn absorb(
    vault_state: &mut VaultState,
    ticks: &mut Vec<Tick>,
    tick_has_debts: &mut Vec<TickHasDebtArray>,
    branches: &mut Vec<Branch>,
    new_branch: &mut Branch,
    max_tick: i32,
) -> anyhow::Result<(i128, i128)> {
    let (next_tick, mut col_absorbed, mut debt_absorbed) = fetch_next_tick_absorb(
        tick_has_debts,
        ticks,
        vault_state.topmost_tick.safe_add(1)?,
        max_tick,
    )?;

    // Process branches
    let mut branch_data = BranchMemoryVars::default();
    let mut new_branch_id = 0; // If this remains 0, it means create a new branch

    branch_data.id = vault_state.current_branch_id;
    let branch = branches
        .iter()
        .find(|branch| branch.branch_id == branch_data.id)
        .ok_or(ErrorCodes::VaultBranchNotFound)?;

    branch_data.set_branch_data(&branch)?;

    if !vault_state.is_branch_liquidated() {
        // Current branch is not liquidated, can be used as a new branch if needed
        new_branch_id = branch_data.id;

        // Check base branch minima tick
        if branch_data.data.connected_minima_tick != TickMath::COLD_TICK {
            // Setting the base branch as current liquidatable branch
            branch_data.id = branch_data.data.connected_branch_id;
            branch_data.minima_tick = branch_data.data.connected_minima_tick;
            let branch = branches
                .iter()
                .find(|branch| branch.branch_id == branch_data.id)
                .ok_or(ErrorCodes::VaultBranchNotFound)?;

            branch_data.set_branch_data(&branch)?;
        } else {
            // The current branch is base branch, need to setup a new base branch
            branch_data.id = 0;
            branch_data.reset_branch_data();
            branch_data.minima_tick = TickMath::COLD_TICK;
        }
    } else {
        // Current branch is liquidated
        branch_data.minima_tick = branch_data.data.minima_tick;
    }

    while branch_data.minima_tick > max_tick {
        // Check base branch, if exists then check if minima tick is above max tick then liquidate it.
        let current_ratio = branch_data.get_current_ratio_from_minima_tick()?;
        let branch_debt: u128 = branch_data.data.debt_liquidity.cast()?;

        // Absorb branch's debt & collateral
        debt_absorbed = debt_absorbed.safe_add(branch_debt)?;
        col_absorbed = col_absorbed.safe_add(
            branch_debt
                .safe_mul(TickMath::ZERO_TICK_SCALED_RATIO)?
                .safe_div(current_ratio)?,
        )?;

        // Close the branch (mark as status 3)
        let branch_to_update = branches
            .iter_mut()
            .find(|b| b.branch_id == branch_data.id)
            .ok_or(ErrorCodes::VaultBranchNotFound)?;

        branch_to_update.set_state_after_absorb(&branch_data.data);

        // Find the next branch to process
        if branch_data.data.connected_minima_tick != TickMath::COLD_TICK {
            // Set the base branch as current liquidatable branch
            branch_data.id = branch_data.data.connected_branch_id;
            branch_data.minima_tick = branch_data.data.connected_minima_tick;
            let branch = branches
                .iter()
                .find(|branch| branch.branch_id == branch_data.id)
                .ok_or(ErrorCodes::VaultBranchNotFound)?;

            branch_data.set_branch_data(&branch)?;
        } else {
            // The current branch is base branch, no more branches to process
            branch_data.id = 0;
            branch_data.reset_branch_data();
            branch_data.minima_tick = TickMath::COLD_TICK;
        }
    }

    // Update vault state based on next tick and branch status
    if next_tick >= branch_data.minima_tick {
        // New top tick is not liquidated
        if next_tick > TickMath::COLD_TICK {
            vault_state.topmost_tick = next_tick;
        } else {
            vault_state.reset_top_tick();
        }

        let init_new_branch = new_branch_id == 0;
        if init_new_branch {
            vault_state.update_branch_info_by_one();
            new_branch_id = vault_state.total_branch_id;
        } else {
            // using already initialized non liquidated branch
            vault_state.reset_branch_liquidated();
        }

        if branch_data.minima_tick > TickMath::COLD_TICK {
            if new_branch_id != new_branch.branch_id {
                return Err(ErrorCodes::VaultNewBranchInvalid.into());
            }

            new_branch.reset_branch_data();
            new_branch.set_connections(branch_data.id, branch_data.minima_tick)?;
        } else {
            let branch_to_clear = if init_new_branch {
                if new_branch_id != new_branch.branch_id {
                    return Err(ErrorCodes::VaultNewBranchInvalid.into());
                }

                new_branch
            } else {
                branches
                    .iter_mut()
                    .find(|branch| branch.branch_id == new_branch_id)
                    .ok_or(ErrorCodes::VaultBranchNotFound)?
            };

            branch_to_clear.reset_branch_data();
        }
    } else {
        // New top tick is liquidated
        vault_state.update_state_at_liq_end(branch_data.minima_tick, branch_data.id)?;

        if new_branch_id != 0 {
            vault_state.total_branch_id = new_branch_id.safe_sub(1)?; // decreasing total branch by 1

            let branch_to_clear = branches
                .iter_mut()
                .find(|branch| branch.branch_id == new_branch_id)
                .ok_or(ErrorCodes::VaultBranchNotFound)?;

            if new_branch_id != branch_to_clear.branch_id {
                return Err(ErrorCodes::VaultNewBranchInvalid.into());
            }

            branch_to_clear.reset_branch_data();
        }
    }

    vault_state.add_absorbed_col_amount(col_absorbed)?;
    vault_state.add_absorbed_debt_amount(debt_absorbed)?;

    Ok((col_absorbed.cast()?, debt_absorbed.cast()?))
}

pub async fn get_ticks_from_oracle_price(
    vault_config: &VaultConfig,
    supply_ex_price: u128,
    borrow_ex_price: u128,
    remaining_accounts: &Vec<AccountMeta>,
    remaining_accounts_indices: &Vec<u8>,
    exchange_rate: u128,
) -> anyhow::Result<(u128, i32, i32)> {
    let start_index: usize = 0;
    let end_index: usize = start_index + remaining_accounts_indices[0].cast::<usize>()?;

    if remaining_accounts.len() < end_index {
        return Err(ErrorCodes::VaultLiquidateRemainingAccountsTooShort.into());
    }

    // Note if price would come back as 0 `get_tick_at_ratio` will fail
    if exchange_rate > 10u128.pow(24) || exchange_rate == 0 {
        // capping to 1B USD per 1 Bitcoin at 15 oracle precision
        return Err((ErrorCodes::VaultInvalidOraclePrice).into());
    }

    // max possible debt_per_col = 1e24 * 1e19 / 1e12 so 1e31. must be done in u256 to avoid potential overflow.
    // (exchange prices can only ever increase ensured in load_exchange_prices)
    let mut debt_per_col: u128 =
        safe_multiply_divide(exchange_rate, supply_ex_price, borrow_ex_price)?;

    if debt_per_col == 0 {
        return Err((ErrorCodes::VaultInvalidOraclePrice).into());
    }

    // capping oracle pricing to 1e26 after applying exchange prices to guarantee enough precision
    if debt_per_col > 10u128.pow(26) {
        debt_per_col = 10u128.pow(26);
    }

    // Raw colPerDebt in 15 decimals, at minimum this comes out at 1e4 precision
    let raw_col_per_debt: u128 = 10u128
        .pow(RATE_OUTPUT_DECIMALS * 2)
        .safe_div(debt_per_col)?;

    // debt_per_col max possible = 1e26, min possible = 21646 (does not reach MIN_RATIO, see below)
    // raw_col_per_debt max possible = 1e30 / 21646 = ~4e25, min possible = 1e4

    // Calculate col_per_debt with liquidation penalty
    // Liquidation penalty in 4 decimals (1e2 = 1%)
    let col_per_debt: u128 = raw_col_per_debt
        .safe_mul(FOUR_DECIMALS.safe_add(vault_config.liquidation_penalty.cast()?)?)?
        .safe_div(FOUR_DECIMALS)?;

    // considering "ratioX48" supports between 6093 and 13002088133096036565414295:

    // when debt_per_col = 1 we do 281474976710656 / 1e15 = 0. <- below MIN RATIO, would error in get_tick_at_ratio()
    // error for any where debt_per_col < ~22000 (x * 281474976710656 / 1e15 = 6093 -> x = 21646, plus consider LT)

    // debt_per_col must max ever end up so that 4e25 * 281474976710656 / 1e15 = 11258999068426240000000000
    //                             fits into max ratio of                        13002088133096036565414295
    // setting to allow up to max 1e26 to cover range of possibilities around liquidation threshold.

    // Calculate liquidation tick (tick at liquidation threshold ratio)
    // Convert debt_per_col to get ratio at liquidation threshold
    let liquidation_ratio: u128 = safe_multiply_divide(
        debt_per_col,
        TickMath::ZERO_TICK_SCALED_RATIO,
        10u128.pow(RATE_OUTPUT_DECIMALS),
    )?;

    // Liquidation threshold in 3 decimals (900 = 90%)
    let threshold_ratio: u128 = liquidation_ratio
        .safe_mul(vault_config.liquidation_threshold.cast()?)?
        .safe_div(THREE_DECIMALS)?;

    let (liquidation_tick, _) = TickMath::get_tick_at_ratio(threshold_ratio)?;

    // Calculate liquidation max limit tick (tick at max limit ratio)
    let max_ratio: u128 = liquidation_ratio
        .safe_mul(vault_config.liquidation_max_limit.cast()?)?
        .safe_div(THREE_DECIMALS)?;

    // get liquidation max limit tick (tick at liquidation max limit ratio)
    let (max_tick, _) = TickMath::get_tick_at_ratio(max_ratio)?;

    Ok((col_per_debt, liquidation_tick, max_tick))
}

pub async fn get_oracle(
    oracle: Pubkey,
    program: &VaultProgram,
) -> anyhow::Result<vaults::accounts::Oracle> {
    let oracle_data: vaults::accounts::Oracle = program.account(oracle).await?;

    Ok(oracle_data)
}

#[derive(Debug, Clone)]
pub struct VaultOraclePriceLiquidate {
    pub price: u128,
    pub sources: Vec<Sources>,
}

pub async fn get_oracle_price_liquidate_from_remaining_accounts(
    oracle: Pubkey,
    remaining_accounts: &Vec<AccountMeta>,
    remaining_accounts_indices: &Vec<u8>,
    program: &VaultProgram,
) -> anyhow::Result<VaultOraclePriceLiquidate> {
    let oracle_data = get_oracle(oracle, program).await?;
    let rpc = program.rpc();
    let cluster = Cluster::Custom(rpc.url(), rpc.url());
    let provider = Client::new_with_options(
        cluster,
        Arc::new(ReadKeypair::from_pubkey(program.payer())),
        rpc.commitment(),
    );
    let oracle_program = provider.program(ORACLE_PROGRAM_ID)?;

    let mut price = 0;

    let start_index: usize = 0;
    let end_index: usize = start_index + remaining_accounts_indices[0].cast::<usize>()?;

    if remaining_accounts.len() < end_index {
        return Err(ErrorCodes::VaultLiquidateRemainingAccountsTooShort.into());
    }

    let remaining_accounts = remaining_accounts
        .iter()
        .take(end_index)
        .skip(start_index)
        .map(|x| x.clone())
        .collect::<Vec<_>>();

    let instructions = oracle_program
        .request()
        .accounts(oracle::client::accounts::GetExchangeRateLiquidate { oracle })
        .accounts(remaining_accounts)
        .args(oracle::client::args::GetExchangeRateLiquidate {
            _nonce: oracle_data.nonce,
        })
        .instructions()?;

    let message = Message::new(&instructions, Some(&oracle_program.payer()));
    let transaction = Transaction::new_unsigned(message);

    let simulation = oracle_program
        .rpc()
        .simulate_transaction_with_config(
            &transaction,
            RpcSimulateTransactionConfig {
                replace_recent_blockhash: true,
                sig_verify: false,
                ..Default::default()
            },
        )
        .await?;

    if let Some(data) = &simulation.value.return_data {
        #[allow(deprecated)]
        let raw_bytes = base64::decode(&data.data.0)?;
        price = u128::from_le_bytes(raw_bytes.try_into().unwrap());
    }

    Ok(VaultOraclePriceLiquidate {
        price,
        sources: oracle_data.sources,
    })
}

pub async fn get_oracle_price_liquidate(
    oracle: Pubkey,
    program: &VaultProgram,
) -> anyhow::Result<VaultOraclePriceLiquidate> {
    let oracle_data = get_oracle(oracle, program).await?;
    let rpc = program.rpc();
    let cluster = Cluster::Custom(rpc.url(), rpc.url());
    let provider = Client::new_with_options(
        cluster,
        Arc::new(ReadKeypair::from_pubkey(program.payer())),
        rpc.commitment(),
    );
    let oracle_program = provider.program(ORACLE_PROGRAM_ID)?;

    let mut remaining_accounts = vec![];

    for source in &oracle_data.sources {
        remaining_accounts.push(AccountMeta::new_readonly(source.source, false));
    }

    let remaining_accounts_indices = vec![remaining_accounts.len() as u8];

    let mut price = 0;

    let start_index: usize = 0;
    let end_index: usize = start_index + remaining_accounts_indices[0].cast::<usize>()?;

    if remaining_accounts.len() < end_index {
        return Err(ErrorCodes::VaultLiquidateRemainingAccountsTooShort.into());
    }

    let remaining_accounts = remaining_accounts
        .iter()
        .take(end_index)
        .skip(start_index)
        .map(|x| x.clone())
        .collect::<Vec<_>>();

    let instructions = oracle_program
        .request()
        .accounts(oracle::client::accounts::GetExchangeRateLiquidate { oracle })
        .accounts(remaining_accounts)
        .args(oracle::client::args::GetExchangeRateLiquidate {
            _nonce: oracle_data.nonce,
        })
        .instructions()?;

    let message = Message::new(&instructions, Some(&oracle_program.payer()));
    let transaction = Transaction::new_unsigned(message);

    let simulation = oracle_program
        .rpc()
        .simulate_transaction_with_config(
            &transaction,
            RpcSimulateTransactionConfig {
                replace_recent_blockhash: true,
                sig_verify: false,
                ..Default::default()
            },
        )
        .await?;

    if let Some(data) = &simulation.value.return_data {
        #[allow(deprecated)]
        let raw_bytes = base64::decode(&data.data.0)?;
        price = u128::from_le_bytes(raw_bytes.try_into().unwrap());
    }

    Ok(VaultOraclePriceLiquidate {
        price,
        sources: oracle_data.sources,
    })
}
