use anchor_client::{
    solana_client::nonblocking::rpc_client::RpcClient,
    solana_sdk::{
        address_lookup_table::state::AddressLookupTable, message::AddressLookupTableAccount,
    },
};
use anchor_lang::prelude::{AccountMeta, Pubkey};

use crate::{
    math::{casting::Cast, safe_math::SafeMath, tick::TickMath, u256::safe_multiply_divide},
    programs::vaults::accounts::{Branch, Tick, TickHasDebtArray, VaultState},
};

use super::{
    constants::*,
    errors::ErrorCodes,
    pda,
    state::{BranchMemoryVars, CurrentLiquidity, TickMemoryVars},
    ticks::{get_tick_indices, get_tick_partials, MIN_TICK, TICK_HAS_DEBT_ARRAY_SIZE},
    VaultProgram,
};

pub async fn load_relevant_branches_for_liquidate(
    vault_id: u16,
    vault_state: VaultState,
    program: &VaultProgram,
) -> anyhow::Result<Vec<Branch>> {
    let mut branches = vec![];

    let current_branch_id = vault_state.current_branch_id;

    if current_branch_id > 0 {
        if let Ok(current_branch) = program
            .account::<Branch>(pda::get_branch(vault_id, current_branch_id))
            .await
        {
            branches.push(current_branch);
        }
    }

    if !branches.is_empty() {
        let mut connected_branch_id = branches[0].connected_branch_id;

        while !branches.iter().any(|b| b.branch_id == connected_branch_id) {
            if let Ok(connected_branch) = program
                .account::<Branch>(pda::get_branch(vault_id, connected_branch_id))
                .await
            {
                connected_branch_id = connected_branch.connected_branch_id;
                branches.push(connected_branch);
            } else {
                break;
            }
        }
    }

    if !branches.iter().any(|b| b.branch_id == 0) {
        branches.push(Branch {
            vault_id,
            branch_id: 0,
            ..Default::default()
        });
    }

    Ok(branches)
}

async fn find_next_tick_with_debt(
    vault_id: u16,
    start_tick: i32,
    program: &VaultProgram,
) -> anyhow::Result<i32> {
    let (array_index, map_index, byte_index, bit_index) = get_tick_indices(start_tick)?;

    let mut current_array_index = array_index;
    let mut current_map_index = map_index;

    let mut tick_has_debt_data = match program
        .account::<TickHasDebtArray>(pda::get_tick_has_debt(vault_id, current_array_index))
        .await
    {
        Ok(data) => data,
        Err(_) => return Ok(MIN_TICK),
    };

    tick_has_debt_data.clear_bits(map_index, byte_index, bit_index)?;

    loop {
        let (next_tick, has_next_tick) =
            tick_has_debt_data.fetch_next_top_tick(current_map_index)?;

        if has_next_tick && next_tick != MIN_TICK {
            return Ok(next_tick);
        }

        // No bits found in current array, move to previous array (lower ticks)
        if current_array_index == 0 {
            return Ok(MIN_TICK);
        }

        current_array_index -= 1;
        current_map_index = TICK_HAS_DEBT_ARRAY_SIZE - 1;

        tick_has_debt_data = match program
            .account::<TickHasDebtArray>(pda::get_tick_has_debt(vault_id, current_array_index))
            .await
        {
            Ok(data) => data,
            Err(_) => return Ok(MIN_TICK),
        };
    }
}
pub async fn load_relevant_ticks_for_liquidate(
    vault_id: u16,
    vault_state: VaultState,
    liquidation_tick: i32,
    program: &VaultProgram,
) -> anyhow::Result<(Vec<Tick>, i32)> {
    let mut ticks = vec![];

    let top_tick = vault_state.topmost_tick;

    if top_tick > liquidation_tick {
        if let Ok(current_tick) = program
            .account::<Tick>(pda::get_tick(vault_id, top_tick))
            .await
        {
            ticks.push(current_tick);
        }
    }

    let mut next_tick = find_next_tick_with_debt(vault_id, top_tick, &program).await?;

    if !ticks.is_empty() {
        while next_tick > liquidation_tick && !ticks.iter().any(|t| t.tick == next_tick) {
            if let Ok(next_tick_data) = program
                .account::<Tick>(pda::get_tick(vault_id, next_tick))
                .await
            {
                ticks.push(next_tick_data);
                if let Ok(tick) = find_next_tick_with_debt(vault_id, next_tick, &program).await {
                    next_tick = tick
                } else {
                    break;
                }
            } else {
                break;
            }
        }
    }

    Ok((ticks, next_tick))
}

pub async fn load_relevant_ticks_has_debt_for_liquidate(
    vault_id: u16,
    top_tick: i32,
    next_tick: i32,
    program: &VaultProgram,
) -> anyhow::Result<Vec<TickHasDebtArray>> {
    let (top_tick_index, _, _, _) = get_tick_indices(top_tick)?;
    let (next_tick_index, _, _, _) = get_tick_indices(next_tick)?;

    let indices: Vec<u8> = (next_tick_index..=top_tick_index).rev().collect();

    let tick_has_debt_array: Vec<TickHasDebtArray> = {
        let futures: Vec<_> = indices
            .iter()
            .map(|&arr_idx| {
                program.account::<TickHasDebtArray>(pda::get_tick_has_debt(vault_id, arr_idx))
            })
            .collect();

        let results = futures::future::join_all(futures).await;

        results
            .into_iter()
            .filter_map(|result| result.ok())
            .collect()
    };

    Ok(tick_has_debt_array)
}

pub async fn fetch_table_accounts(
    client: &RpcClient,
    table_addresses: Vec<Pubkey>,
) -> Vec<AddressLookupTableAccount> {
    let tasks: Vec<_> = table_addresses
        .iter()
        .map(async |address_lookup_table_key| {
            match client.get_account(&address_lookup_table_key).await {
                Ok(raw_account) => {
                    let address_lookup_table =
                        match AddressLookupTable::deserialize(&raw_account.data) {
                            Ok(table) => table,
                            Err(_) => return None,
                        };
                    let address_lookup_table_account = AddressLookupTableAccount {
                        key: address_lookup_table_key.clone(),
                        addresses: address_lookup_table.addresses.to_vec(),
                    };
                    Some(address_lookup_table_account)
                }
                Err(_) => None,
            }
        })
        .collect();

    let results = futures::future::join_all(tasks).await;

    results.into_iter().filter_map(|result| result).collect()
}

pub async fn get_branches_from_remaining_accounts(
    remaining_accounts: &Vec<AccountMeta>,
    remaining_accounts_indices: &Vec<u8>,
    program: &VaultProgram,
) -> anyhow::Result<Vec<Branch>> {
    // remaining_accounts_indices[0] is oracle sources length
    // remaining_accounts_indices[1] is branches length

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

    Ok(futures::future::try_join_all(
        remaining_accounts
            .iter()
            .take(end_index)
            .skip(start_index)
            .map(|account| program.account::<Branch>(account.pubkey)),
    )
    .await?)
}

#[allow(clippy::too_many_arguments)]
pub fn end_liquidate<'info>(
    current_data: &mut CurrentLiquidity,
    tick_info: &mut TickMemoryVars,
    branch_in_memory: &mut BranchMemoryVars,
    debt_liquidated: &mut u128,
    col_liquidated: &mut u128,
    col_per_debt: u128,
    minimum_branch_debt: u128,
) -> anyhow::Result<()> {
    if *debt_liquidated >= current_data.debt_remaining {
        // Liquidation ended between currentTick & refTick
        // Not all of liquidatable debt is actually liquidated -> recalculate
        *debt_liquidated = current_data.debt_remaining;
        // debt_liquidated here related to input param liquidation amountm so using u256 safe multiply to be sure no overflow can happen
        *col_liquidated = safe_multiply_divide(
            *debt_liquidated,
            col_per_debt,
            10u128.pow(RATE_OUTPUT_DECIMALS),
        )?;

        // Liquidating to debt. Calculate final ratio after liquidation
        // liquidatable debt - debtLiquidated / liquidatable col - colLiquidated
        let final_ratio: u128 = current_data.get_final_ratio(*col_liquidated, *debt_liquidated)?;

        // Fetching tick of where liquidation ended
        let (mut final_tick, ratio_one_less) = TickMath::get_tick_at_ratio(final_ratio)?;

        if (final_tick < current_data.ref_tick) && (tick_info.partials == X30) {
            // This situation might never happen
            // If this happens then there might be some very edge case precision of few weis which is returning 1 tick less
            // If the above were to ever happen then tickInfo_.tick only be currentData_.refTick - 1
            // In this case the partial will be very very near to full (X30)
            // Increasing tick by 2 and making partial as 1 which is basically very very near to currentData_.refTick
            final_tick = final_tick.safe_add(2)?;
            tick_info.set_partials(1)?;
        } else {
            // Increasing tick by 1 as final ratio will probably be a partial
            final_tick = final_tick.safe_add(1)?;

            let existing_partials =
                if current_data.is_ref_tick_liquidated() && final_tick == current_data.ref_tick {
                    tick_info.partials
                } else {
                    0
                };

            // Taking edge cases where partial comes as 0 or X30 meaning perfect tick
            // Hence, increasing or reducing it by 1 as liquidation tick cannot be perfect tick
            tick_info.set_partials(get_tick_partials(ratio_one_less, final_ratio)?)?;

            current_data
                .check_is_ref_partials_safe_for_tick(existing_partials, tick_info.partials)?;
        }

        tick_info.set_tick(final_tick);
    } else {
        // End in liquidation threshold
        // finalRatio_ = current_data.ref_ratio
        // Increasing liquidation threshold tick by 1 partial. With 1 partial it'll reach to the next tick
        // Ratio change will be negligible. Doing this as liquidation threshold tick can also be a perfect non-liquidated tick
        tick_info.set_tick(current_data.ref_tick.safe_add(1)?);

        // Making partial as 1 so it doesn't stay perfect tick
        tick_info.set_partials(1)?;
        // Length is not needed as only partials are written to storage
    }

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

    current_data.update_totals(*debt_liquidated, *col_liquidated)?;

    // Updating branch's debt factor & write to storage as liquidation is over
    // Using branch.debt_factor.mul_div_big_number() to match the Solidity .mulDivBigNumber()
    branch_in_memory.update_branch_debt_factor(debt_factor)?;

    if current_data.debt < minimum_branch_debt {
        // This can happen when someone tries to create a dust tick
        return Err(ErrorCodes::VaultBranchDebtTooLow.into());
    }

    Ok(())
}
