#![allow(clippy::collapsible_else_if)]
#![allow(clippy::too_many_arguments)]

use crate::{CoreError, HUNDRED_PERCENT, INVALID_ARGUMENTS};
use fusionamm_core::{tick_index_to_sqrt_price, try_get_amount_delta_a, try_get_amount_delta_b, Q64_RESOLUTION, U128};

#[cfg(feature = "wasm")]
use fusionamm_macros::wasm_expose;

#[derive(Debug, Copy, Clone, PartialEq)]
#[cfg_attr(feature = "wasm", wasm_expose)]
pub struct LiquidationPrices {
    pub lower: f64,
    pub upper: f64,
}

///
/// Calculates the liquidation prices inside the position's range.
///
/// t - liquidation threshold
/// Dx - debt x
/// Dy - debt y
/// Lx - leftovers x
/// Ly - leftovers y
/// √P = x - current price
/// √Pu = u - upper sqrt price
/// √Pl = l - lower sqrt price
/// L - liquidity
///
/// t = (Dx⋅P + Dy) / (Δx⋅P + Lx⋅P + Δy + Ly), where
///   Δx = L⋅(1/x - 1/u)
///   Δy = L⋅(x - l)
///
/// x1,2 = ...
///
/// # Parameters
/// - `lower_sqrt_price`: The lower square root price boundary.
/// - `upper_sqrt_price`: The upper square root price boundary.
/// - `liquidity`: The liquidity provided by the user.
/// - `leftovers_a`: The amount of leftovers A in the existing position.
/// - `leftovers_b`: The amount of leftovers B in the existing position*
/// - `debt_a`: The amount of tokens A borrowed.
/// - `debt_b`: The amount of tokens B borrowed.
/// - `liquidation_threshold`: The liquidation threshold of the liquidator.
///
/// # Returns
/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
fn compute_liquidation_prices_inside(
    lower_sqrt_price: u128,
    upper_sqrt_price: u128,
    liquidity: u128,
    leftovers_a: u64,
    leftovers_b: u64,
    debt_a: u64,
    debt_b: u64,
    liquidation_threshold: u32,
) -> Result<LiquidationPrices, CoreError> {
    let liquidation_threshold_f = liquidation_threshold as f64 / HUNDRED_PERCENT as f64;
    let liquidity_f = liquidity as f64;
    let lower_sqrt_price_f = lower_sqrt_price as f64 / Q64_RESOLUTION;
    let upper_sqrt_price_f = upper_sqrt_price as f64 / Q64_RESOLUTION;

    let a = debt_a as f64 + liquidation_threshold_f * (liquidity_f / upper_sqrt_price_f - leftovers_a as f64);
    let b = -2.0 * liquidation_threshold_f * liquidity_f;
    let c = debt_b as f64 + liquidation_threshold_f * (liquidity_f * lower_sqrt_price_f - leftovers_b as f64);
    let d = b * b - 4.0 * a * c;

    let mut lower_liquidation_sqrt_price = 0.0;
    let mut upper_liquidation_sqrt_price = 0.0;

    if d >= 0.0 {
        lower_liquidation_sqrt_price = (-b - d.sqrt()) / (2.0 * a);
        upper_liquidation_sqrt_price = (-b + d.sqrt()) / (2.0 * a);
        if lower_liquidation_sqrt_price < 0.0 || lower_liquidation_sqrt_price < lower_sqrt_price_f {
            lower_liquidation_sqrt_price = 0.0;
        }
        if upper_liquidation_sqrt_price < 0.0 || upper_liquidation_sqrt_price > upper_sqrt_price_f {
            upper_liquidation_sqrt_price = 0.0;
        }
    }

    Ok(LiquidationPrices {
        lower: lower_liquidation_sqrt_price * lower_liquidation_sqrt_price,
        upper: upper_liquidation_sqrt_price * upper_liquidation_sqrt_price,
    })
}

/// Calculates a liquidation price for the outside range (above OR under).
///
/// liquidation_threshold = total_debt / (total_balance + leftovers)
///
/// t - liquidation threshold
/// P - liquidation price
/// Dx - debt x
/// Dy - debt y
/// Lx - leftovers x
/// Ly - leftovers y
/// X - amount of x tokens
/// Y - amount of y tokens
///
/// Lower liquidation price:
/// t = (Dx + Dy / P) / (X + Lx + Ly/P)  =>  P = (Dy - t⋅Ly) / (t⋅(X+Lx) - Dx)
///
/// Upper liquidation price:
/// t = (Dx⋅P + Dy) / (Y + Lx⋅P + Ly)  =>  P = (t⋅(Y + Ly) - Dy) / (Dx - t⋅Lx)
///
/// # Parameters
/// - `amount_a`: The amount of tokens A at the boundary price.
/// - `amount_b`: The amount of tokens B at the boundary price.
/// - `leftovers_a`: The amount of leftovers A.
/// - `leftovers_b`: The amount of leftovers B.
/// - `debt_a`: The amount of tokens A borrowed.
/// - `debt_b`: The amount of tokens B borrowed.
/// - `liquidation_threshold`: - The liquidation threshold of the liquidator.
///
/// # Returns
/// The calculated liquidation price.
fn calculate_liquidation_outside(
    amount_a: u64,
    amount_b: u64,
    leftovers_a: u64,
    leftovers_b: u64,
    debt_a: u64,
    debt_b: u64,
    liquidation_threshold: u32,
) -> Result<f64, CoreError> {
    let liquidation_threshold_f = liquidation_threshold as f64 / HUNDRED_PERCENT as f64;

    if amount_a == 0 && amount_b == 0 {
        Ok(0.0)
    } else if amount_a > 0 && amount_b == 0 {
        // Lower liquidation price
        let numerator = debt_b as f64 - liquidation_threshold_f * leftovers_b as f64;
        let denominator = liquidation_threshold_f * (amount_a + leftovers_a) as f64 - debt_a as f64;
        Ok(numerator / denominator)
    } else if amount_a == 0 && amount_b > 0 {
        // Upper liquidation price
        let numerator = liquidation_threshold_f * (amount_b + leftovers_b) as f64 - debt_b as f64;
        let denominator = debt_a as f64 - liquidation_threshold_f * leftovers_a as f64;
        if denominator == 0.0 {
            return Ok(0.0);
        }
        Ok(numerator / denominator)
    } else {
        Err(INVALID_ARGUMENTS)
    }
}

/// Calculates the liquidation prices outside the position's range (above AND under).
///
/// # Parameters
/// - `lower_sqrt_price`: The lower square root price boundary.
/// - `upper_sqrt_price`: The upper square root price boundary.
/// - `liquidity`: The liquidity provided by the user.
/// - `leftovers_a`: The amount of leftovers A.
/// - `leftovers_b`: The amount of leftovers B.///
/// - `debt_a`: The amount of tokens A borrowed.
/// - `debt_b`: The amount of tokens B borrowed.
/// - `liquidation_threshold`: The liquidation threshold of the liquidator.
///
/// # Returns
/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
fn compute_liquidation_prices_outside(
    lower_sqrt_price: u128,
    upper_sqrt_price: u128,
    liquidity: u128,
    leftovers_a: u64,
    leftovers_b: u64,
    debt_a: u64,
    debt_b: u64,
    liquidation_threshold: u32,
) -> Result<LiquidationPrices, CoreError> {
    let amount_a = try_get_amount_delta_a(lower_sqrt_price.into(), upper_sqrt_price.into(), liquidity.into(), false)?;
    let amount_b = try_get_amount_delta_b(lower_sqrt_price.into(), upper_sqrt_price.into(), liquidity.into(), false)?;

    let mut liquidation_price_for_a = calculate_liquidation_outside(amount_a, 0, leftovers_a, leftovers_b, debt_a, debt_b, liquidation_threshold)?;
    let mut liquidation_price_for_b = calculate_liquidation_outside(0, amount_b, leftovers_a, leftovers_b, debt_a, debt_b, liquidation_threshold)?;

    if liquidation_price_for_a < 0.0 || liquidation_price_for_a > (lower_sqrt_price as f64 / Q64_RESOLUTION).powf(2.0) {
        liquidation_price_for_a = 0.0;
    }
    if liquidation_price_for_b < 0.0 || liquidation_price_for_b < (upper_sqrt_price as f64 / Q64_RESOLUTION).powf(2.0) {
        liquidation_price_for_b = 0.0;
    }

    Ok(LiquidationPrices {
        lower: liquidation_price_for_a,
        upper: liquidation_price_for_b,
    })
}

/// Computes the liquidation prices for an existing position.
///
/// # Parameters
/// - `lower_tick_index`: The lower tick index of the position.
/// - `upper_tick_index`: The upper tick index of the position.
/// - `leftovers_a`: The amount of leftovers A in the position.
/// - `leftovers_a`: The amount of leftovers B in the position.
/// - `liquidity`: Liquidity of the position.
/// - `debt_a`: The amount of tokens A borrowed.
/// - `debt_b`: The amount of tokens B borrowed.
/// - `liquidation_threshold`: The liquidation threshold of the market.
///
/// # Returns
/// - `LiquidationPrices`: An object containing lower/upper liquidation prices.
#[cfg_attr(feature = "wasm", wasm_expose)]
pub fn get_lp_position_liquidation_prices(
    lower_tick_index: i32,
    upper_tick_index: i32,
    liquidity: U128,
    leftovers_a: u64,
    leftovers_b: u64,
    debt_a: u64,
    debt_b: u64,
    liquidation_threshold: u32,
) -> Result<LiquidationPrices, CoreError> {
    if lower_tick_index >= upper_tick_index {
        return Err(INVALID_ARGUMENTS);
    }

    if liquidation_threshold >= HUNDRED_PERCENT {
        return Err(INVALID_ARGUMENTS);
    }

    let lower_sqrt_price: u128 = tick_index_to_sqrt_price(lower_tick_index).into();
    let upper_sqrt_price: u128 = tick_index_to_sqrt_price(upper_tick_index).into();
    let liquidity: u128 = liquidity.into();

    let liquidation_price_inside = compute_liquidation_prices_inside(
        lower_sqrt_price,
        upper_sqrt_price,
        liquidity,
        leftovers_a,
        leftovers_b,
        debt_a,
        debt_b,
        liquidation_threshold,
    )?;

    let liquidation_price_outside = compute_liquidation_prices_outside(
        lower_sqrt_price,
        upper_sqrt_price,
        liquidity,
        leftovers_a,
        leftovers_b,
        debt_a,
        debt_b,
        liquidation_threshold,
    )?;

    let lower_liquidation_price = if liquidation_price_inside.lower > 0.0 {
        liquidation_price_inside.lower
    } else {
        liquidation_price_outside.lower
    };

    let upper_liquidation_price = if liquidation_price_inside.upper > 0.0 {
        liquidation_price_inside.upper
    } else {
        liquidation_price_outside.upper
    };

    Ok(LiquidationPrices {
        lower: lower_liquidation_price,
        upper: upper_liquidation_price,
    })
}

#[cfg(all(test, not(feature = "wasm")))]
mod tests {
    use crate::{get_lp_position_liquidation_prices, LiquidationPrices, HUNDRED_PERCENT};
    use fusionamm_core::{price_to_sqrt_price, price_to_tick_index, tick_index_to_sqrt_price, try_get_liquidity_from_b};
    use once_cell::sync::Lazy;

    pub static LOWER_TICK_INDEX: Lazy<i32> = Lazy::new(|| price_to_tick_index(180.736, 6, 6));
    pub static UPPER_TICK_INDEX: Lazy<i32> = Lazy::new(|| price_to_tick_index(225.66, 6, 6));
    pub static SQRT_PRICE: Lazy<u128> = Lazy::new(|| price_to_sqrt_price(213.41, 6, 6));
    pub static LIQUIDITY: Lazy<u128> =
        Lazy::new(|| try_get_liquidity_from_b(10000_000_000, tick_index_to_sqrt_price(*LOWER_TICK_INDEX), *SQRT_PRICE).unwrap());

    #[test]
    fn test_liquidation_price_outside_range_lower() {
        assert_eq!(
            get_lp_position_liquidation_prices(
                *LOWER_TICK_INDEX,
                *UPPER_TICK_INDEX,
                *LIQUIDITY,
                0,                          // leftovers_a
                0,                          // leftovers_b
                0,                          // debt_a
                9807_000_000,               // debt_b
                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
            ),
            Ok(LiquidationPrices {
                lower: 176.12815046153585,
                upper: 0.0
            })
        );
    }

    #[test]
    fn test_liquidation_price_outside_range_upper() {
        assert_eq!(
            get_lp_position_liquidation_prices(
                *LOWER_TICK_INDEX,
                *UPPER_TICK_INDEX,
                *LIQUIDITY,
                0,                          // leftovers_a
                0,                          // leftovers_b
                20_000_000,                 // debt_a
                0,                          // debt_b
                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
            ),
            Ok(LiquidationPrices {
                lower: 0.0,
                upper: 562.219410388
            })
        );
    }

    #[test]
    fn test_liquidation_price_outside_range_lower_and_upper() {
        assert_eq!(
            get_lp_position_liquidation_prices(
                *LOWER_TICK_INDEX,
                *UPPER_TICK_INDEX,
                *LIQUIDITY,
                0,                          // leftovers_a
                0,                          // leftovers_b
                10_000_000,                 // debt_a
                5000_000_000,               // debt_b
                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
            ),
            Ok(LiquidationPrices {
                lower: 109.45458168998225,
                upper: 624.4388207760001
            })
        );
    }

    #[test]
    fn test_liquidation_price_inside_range_lower() {
        assert_eq!(
            get_lp_position_liquidation_prices(
                *LOWER_TICK_INDEX,
                *UPPER_TICK_INDEX,
                *LIQUIDITY,
                0,                          // leftovers_a
                0,                          // leftovers_b
                0,                          // debt_a
                11000_000_000,              // debt_b
                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
            ),
            Ok(LiquidationPrices {
                lower: 204.60489065334323,
                upper: 0.0
            })
        );
    }

    #[test]
    fn test_liquidation_price_inside_range_upper() {
        assert_eq!(
            get_lp_position_liquidation_prices(
                *LOWER_TICK_INDEX,
                *UPPER_TICK_INDEX,
                *LIQUIDITY,
                0,                          // leftovers_a
                0,                          // leftovers_b
                51_000_000,                 // debt_a
                0,                          // debt_b
                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
            ),
            Ok(LiquidationPrices {
                lower: 0.0,
                upper: 220.16318077637644
            })
        );
    }

    #[test]
    fn test_liquidation_price_inside_range_lower_and_upper() {
        assert_eq!(
            get_lp_position_liquidation_prices(
                *LOWER_TICK_INDEX,
                *UPPER_TICK_INDEX,
                *LIQUIDITY,
                0,                          // leftovers_a
                0,                          // leftovers_b
                11_500_000,                 // debt_a
                8700_000_000,               // debt_b
                HUNDRED_PERCENT * 83 / 100  // liquidation_threshold
            ),
            Ok(LiquidationPrices {
                lower: 210.75514596082337,
                upper: 219.48595430071575
            })
        );
    }
}
