//! Interest rate calculations for Kamino lending reserves.
//!
//! Ports the Klend smart contract borrow rate curve and utilization logic.
//! All math uses checked arithmetic — no panics in production.

use pyra_types::{KaminoReserve, KAMINO_FRACTION_SCALE};

use crate::{RateError, RateResult, RATE_PRECISION};

/// 100% in basis points.
const BPS_MAX: u128 = 10_000;

// ─── Utilization ─────────────────────────────────────────────────

/// Calculate utilization ratio scaled by [`RATE_PRECISION`].
///
/// Ports `ReserveLiquidity::utilization_rate()` from the Klend smart contract.
/// Returns 0 when total supply is zero.
pub fn calculate_kamino_utilization(reserve: &KaminoReserve) -> RateResult<u128> {
    let available = reserve.liquidity.total_available_amount as u128;
    let borrowed = reserve
        .liquidity
        .borrowed_amount_sf
        .checked_div(KAMINO_FRACTION_SCALE)
        .ok_or(RateError::Overflow)?;
    let protocol_fees = reserve
        .liquidity
        .accumulated_protocol_fees_sf
        .checked_div(KAMINO_FRACTION_SCALE)
        .ok_or(RateError::Overflow)?;
    let referrer_fees = reserve
        .liquidity
        .accumulated_referrer_fees_sf
        .checked_div(KAMINO_FRACTION_SCALE)
        .ok_or(RateError::Overflow)?;
    let pending_referrer_fees = reserve
        .liquidity
        .pending_referrer_fees_sf
        .checked_div(KAMINO_FRACTION_SCALE)
        .ok_or(RateError::Overflow)?;

    let total_supply = available
        .checked_add(borrowed)
        .ok_or(RateError::Overflow)?
        .checked_sub(protocol_fees)
        .ok_or(RateError::Overflow)?
        .checked_sub(referrer_fees)
        .ok_or(RateError::Overflow)?
        .checked_sub(pending_referrer_fees)
        .ok_or(RateError::Overflow)?;

    if total_supply == 0 {
        return Ok(0);
    }

    borrowed
        .checked_mul(RATE_PRECISION)
        .ok_or(RateError::Overflow)?
        .checked_div(total_supply)
        .ok_or(RateError::Overflow)
}

// ─── Borrow rate ─────────────────────────────────────────────────

/// Calculate annualized borrow rate scaled by [`RATE_PRECISION`].
///
/// Ports `BorrowRateCurve::get_borrow_rate()` from the Klend smart contract.
/// Uses the reserve's 11-point piecewise-linear curve with linear
/// interpolation between segments.
///
/// NOTE: The Klend SDK applies a `slotAdjustmentFactor = 1000 / SLOTS_PER_SECOND / recentSlotDurationMs`
/// to convert per-slot rates to annualized. We assume ideal slot time here, which is
/// acceptable for display/caching purposes (rates crate is not used for spending decisions).
pub fn calculate_kamino_borrow_rate(reserve: &KaminoReserve) -> RateResult<u128> {
    let utilization = calculate_kamino_utilization(reserve)?;

    // Convert utilization to bps, capped at 100%.
    let util_bps = utilization
        .checked_mul(BPS_MAX)
        .ok_or(RateError::Overflow)?
        .checked_div(RATE_PRECISION)
        .ok_or(RateError::Overflow)?;
    let util_bps = if util_bps > BPS_MAX {
        BPS_MAX
    } else {
        util_bps
    };

    let points = &reserve.config.borrow_rate_curve.points;
    let mut rate_bps: u128 = 0;

    for i in 1..points.len() {
        let end_util = points[i].utilization_rate_bps as u128;

        // Zero-padded trailing points: use the previous point's rate.
        if end_util == 0 && i > 1 {
            rate_bps = points[i.checked_sub(1).ok_or(RateError::Overflow)?].borrow_rate_bps as u128;
            break;
        }

        if util_bps <= end_util {
            let start_util =
                points[i.checked_sub(1).ok_or(RateError::Overflow)?].utilization_rate_bps as u128;
            let start_rate =
                points[i.checked_sub(1).ok_or(RateError::Overflow)?].borrow_rate_bps as u128;
            let end_rate = points[i].borrow_rate_bps as u128;

            let range = end_util
                .checked_sub(start_util)
                .ok_or(RateError::Overflow)?;

            if range == 0 {
                rate_bps = start_rate;
            } else {
                let util_offset = util_bps
                    .checked_sub(start_util)
                    .ok_or(RateError::Overflow)?;
                // Non-monotonic curves (end_rate < start_rate) are treated as
                // configuration errors.
                let rate_span = end_rate
                    .checked_sub(start_rate)
                    .ok_or(RateError::Overflow)?;

                rate_bps = start_rate
                    .checked_add(
                        util_offset
                            .checked_mul(rate_span)
                            .ok_or(RateError::Overflow)?
                            .checked_div(range)
                            .ok_or(RateError::Overflow)?,
                    )
                    .ok_or(RateError::Overflow)?;
            }
            break;
        }
    }

    // Convert from bps to RATE_PRECISION scale.
    rate_bps
        .checked_mul(RATE_PRECISION)
        .ok_or(RateError::Overflow)?
        .checked_div(BPS_MAX)
        .ok_or(RateError::Overflow)
}

// ─── Deposit rate ────────────────────────────────────────────────

/// Calculate annualized deposit rate scaled by [`RATE_PRECISION`].
///
/// Ports the SDK's `calculateSupplyAPR()`:
/// `deposit_rate = utilization * borrow_rate * (1 - protocol_take_rate_pct / 100)`
pub fn calculate_kamino_deposit_rate(reserve: &KaminoReserve) -> RateResult<u128> {
    let utilization = calculate_kamino_utilization(reserve)?;
    let borrow_rate = calculate_kamino_borrow_rate(reserve)?;
    let take_rate_pct = reserve.config.protocol_take_rate_pct as u128;

    // (1 - take_rate_pct/100) expressed in RATE_PRECISION
    let take_rate_scaled = take_rate_pct
        .checked_mul(RATE_PRECISION)
        .ok_or(RateError::Overflow)?
        .checked_div(100)
        .ok_or(RateError::Overflow)?;
    let depositor_factor = RATE_PRECISION
        .checked_sub(take_rate_scaled)
        .ok_or(RateError::Overflow)?;

    borrow_rate
        .checked_mul(depositor_factor)
        .ok_or(RateError::Overflow)?
        .checked_mul(utilization)
        .ok_or(RateError::Overflow)?
        .checked_div(RATE_PRECISION)
        .ok_or(RateError::Overflow)?
        .checked_div(RATE_PRECISION)
        .ok_or(RateError::Overflow)
}

// ─── LTV ─────────────────────────────────────────────────────────

/// Return the loan-to-value ratio as a decimal fraction.
///
/// `loan_to_value_pct` is a u8 (0-100); this returns e.g. `0.75` for 75%.
pub fn calculate_kamino_ltv(reserve: &KaminoReserve) -> f64 {
    reserve.config.loan_to_value_pct as f64 / 100.0
}

// ─── Convenience helper ─────────────────────────────────────────

/// Compute all Kamino rates, returned as `(deposit_rate, borrow_rate, ltv)` decimals.
///
/// Deposit and borrow rates are fractions (e.g. `0.08` = 8%).
/// LTV is a fraction (e.g. `0.75` = 75%).
pub fn compute_kamino_rates(reserve: &KaminoReserve) -> RateResult<(f64, f64, f64)> {
    let deposit_rate = crate::rate_to_decimal(calculate_kamino_deposit_rate(reserve)?);
    let borrow_rate = crate::rate_to_decimal(calculate_kamino_borrow_rate(reserve)?);
    let ltv = calculate_kamino_ltv(reserve);
    Ok((deposit_rate, borrow_rate, ltv))
}

// ─── Tests ───────────────────────────────────────────────────────

#[cfg(test)]
#[allow(
    clippy::allow_attributes,
    clippy::allow_attributes_without_reason,
    clippy::unwrap_used
)]
mod tests {
    use super::*;
    use pyra_types::{
        KaminoBorrowRateCurve, KaminoCurvePoint, KaminoReserve, KaminoReserveCollateral,
        KaminoReserveConfig, KaminoReserveLiquidity,
    };

    /// Build a reserve with the given liquidity and curve parameters.
    fn make_reserve(
        available: u64,
        borrowed_amount: u128,
        protocol_fees: u128,
        protocol_take_rate_pct: u8,
        ltv_pct: u8,
        curve_points: &[(u32, u32)],
    ) -> KaminoReserve {
        let mut points = [KaminoCurvePoint::default(); 11];
        for (i, &(util, rate)) in curve_points.iter().enumerate() {
            if i < 11 {
                points[i] = KaminoCurvePoint {
                    utilization_rate_bps: util,
                    borrow_rate_bps: rate,
                };
            }
        }

        KaminoReserve {
            liquidity: KaminoReserveLiquidity {
                total_available_amount: available,
                borrowed_amount_sf: borrowed_amount
                    .checked_mul(KAMINO_FRACTION_SCALE)
                    .unwrap_or(0),
                accumulated_protocol_fees_sf: protocol_fees
                    .checked_mul(KAMINO_FRACTION_SCALE)
                    .unwrap_or(0),
                mint_decimals: 6,
                ..Default::default()
            },
            collateral: KaminoReserveCollateral::default(),
            config: KaminoReserveConfig {
                loan_to_value_pct: ltv_pct,
                liquidation_threshold_pct: 85,
                protocol_take_rate_pct,
                borrow_rate_curve: KaminoBorrowRateCurve { points },
                ..Default::default()
            },
            ..Default::default()
        }
    }

    /// Standard 3-point curve: 0% -> 0bps, 80% -> 400bps (4%), 100% -> 10000bps (100%).
    fn standard_curve() -> Vec<(u32, u32)> {
        vec![(0, 0), (8000, 400), (10000, 10000)]
    }

    // ─── Utilization ─────────────────────────────────────────────

    #[test]
    fn utilization_zero_when_empty() {
        let reserve = make_reserve(0, 0, 0, 0, 75, &standard_curve());
        assert_eq!(calculate_kamino_utilization(&reserve).unwrap(), 0);
    }

    #[test]
    fn utilization_zero_when_no_borrows() {
        let reserve = make_reserve(1_000_000, 0, 0, 0, 75, &standard_curve());
        assert_eq!(calculate_kamino_utilization(&reserve).unwrap(), 0);
    }

    #[test]
    fn utilization_fifty_percent() {
        // available=500, borrowed=500, total_supply=1000, util=50%
        let reserve = make_reserve(500, 500, 0, 0, 75, &standard_curve());
        assert_eq!(
            calculate_kamino_utilization(&reserve).unwrap(),
            500_000 // 50% in RATE_PRECISION
        );
    }

    #[test]
    fn utilization_full() {
        // available=0, borrowed=1000, total_supply=1000, util=100%
        let reserve = make_reserve(0, 1000, 0, 0, 75, &standard_curve());
        assert_eq!(
            calculate_kamino_utilization(&reserve).unwrap(),
            RATE_PRECISION
        );
    }

    #[test]
    fn utilization_accounts_for_protocol_fees() {
        // available=400, borrowed=600, protocol_fees=100
        // total_supply = 400 + 600 - 100 = 900
        // utilization = 600 / 900 = 66.67%
        let reserve = make_reserve(400, 600, 100, 0, 75, &standard_curve());
        let util = calculate_kamino_utilization(&reserve).unwrap();
        assert_eq!(util, 666_666); // 66.67% truncated
    }

    // ─── Borrow rate ─────────────────────────────────────────────

    #[test]
    fn borrow_rate_zero_utilization() {
        let reserve = make_reserve(1_000_000, 0, 0, 0, 75, &standard_curve());
        assert_eq!(calculate_kamino_borrow_rate(&reserve).unwrap(), 0);
    }

    #[test]
    fn borrow_rate_at_curve_point() {
        // 80% utilization -> exactly at the second curve point (400 bps = 4%)
        // available=200, borrowed=800, total_supply=1000
        let reserve = make_reserve(200, 800, 0, 0, 75, &standard_curve());
        let rate = calculate_kamino_borrow_rate(&reserve).unwrap();
        // 400 bps * RATE_PRECISION / 10000 = 40_000
        assert_eq!(rate, 40_000);
    }

    #[test]
    fn borrow_rate_interpolated() {
        // 40% utilization -> midpoint of first segment (0% -> 80%)
        // Curve: 0bps at 0%, 400bps at 80%
        // At 40% (4000 bps util): rate = 0 + (4000 * 400 / 8000) = 200 bps
        // available=600, borrowed=400, total_supply=1000
        let reserve = make_reserve(600, 400, 0, 0, 75, &standard_curve());
        let rate = calculate_kamino_borrow_rate(&reserve).unwrap();
        // 200 bps * RATE_PRECISION / 10000 = 20_000
        assert_eq!(rate, 20_000);
    }

    #[test]
    fn borrow_rate_full_utilization() {
        // 100% utilization -> last curve point (10000 bps)
        let reserve = make_reserve(0, 1000, 0, 0, 75, &standard_curve());
        let rate = calculate_kamino_borrow_rate(&reserve).unwrap();
        // 10000 bps * RATE_PRECISION / 10000 = RATE_PRECISION
        assert_eq!(rate, RATE_PRECISION);
    }

    #[test]
    fn borrow_rate_above_optimal_interpolated() {
        // 90% utilization -> midpoint of second segment (80% -> 100%)
        // Curve: 400bps at 80%, 10000bps at 100%
        // At 90% (9000 bps): rate = 400 + (1000 * 9600 / 2000) = 400 + 4800 = 5200 bps
        // available=100, borrowed=900, total_supply=1000
        let reserve = make_reserve(100, 900, 0, 0, 75, &standard_curve());
        let rate = calculate_kamino_borrow_rate(&reserve).unwrap();
        // 5200 bps * RATE_PRECISION / 10000 = 520_000
        assert_eq!(rate, 520_000);
    }

    // ─── Deposit rate ────────────────────────────────────────────

    #[test]
    fn deposit_rate_zero_utilization() {
        let reserve = make_reserve(1_000_000, 0, 0, 0, 75, &standard_curve());
        assert_eq!(calculate_kamino_deposit_rate(&reserve).unwrap(), 0);
    }

    #[test]
    fn deposit_rate_no_take_rate() {
        // 50% util, borrow rate at 50% = 0 + (5000 * 400 / 8000) = 250 bps = 25_000
        // deposit_rate = 500_000 * 25_000 * 1_000_000 / 1e6 / 1e6 = 12_500
        let reserve = make_reserve(500, 500, 0, 0, 75, &standard_curve());
        let rate = calculate_kamino_deposit_rate(&reserve).unwrap();
        assert_eq!(rate, 12_500);
    }

    #[test]
    fn deposit_rate_with_take_rate() {
        // Same as above but 50% protocol take rate
        // deposit_rate = 500_000 * 25_000 * 500_000 / 1e6 / 1e6 = 6_250
        let reserve = make_reserve(500, 500, 0, 50, 75, &standard_curve());
        let rate = calculate_kamino_deposit_rate(&reserve).unwrap();
        assert_eq!(rate, 6_250);
    }

    #[test]
    fn deposit_rate_full_take_rate() {
        // 100% take rate -> depositors get nothing
        let reserve = make_reserve(500, 500, 0, 100, 75, &standard_curve());
        assert_eq!(calculate_kamino_deposit_rate(&reserve).unwrap(), 0);
    }

    // ─── LTV ─────────────────────────────────────────────────────

    #[test]
    fn ltv_75_percent() {
        let reserve = make_reserve(1000, 0, 0, 0, 75, &standard_curve());
        assert!((calculate_kamino_ltv(&reserve) - 0.75).abs() < f64::EPSILON);
    }

    #[test]
    fn ltv_zero() {
        let reserve = make_reserve(1000, 0, 0, 0, 0, &standard_curve());
        assert!((calculate_kamino_ltv(&reserve) - 0.0).abs() < f64::EPSILON);
    }

    // ─── Convenience helper ──────────────────────────────────────

    #[test]
    fn compute_kamino_rates_basic() {
        let reserve = make_reserve(500, 500, 0, 10, 75, &standard_curve());
        let (deposit, borrow, ltv) = compute_kamino_rates(&reserve).unwrap();
        assert!(deposit > 0.0);
        assert!(borrow > 0.0);
        assert!(deposit < borrow);
        assert!((ltv - 0.75).abs() < f64::EPSILON);
    }

    // ─── Proptest ────────────────────────────────────────────────

    mod proptests {
        use super::*;
        use proptest::prelude::*;

        prop_compose! {
            fn arb_reserve()(
                available in 0u64..=1_000_000_000,
                borrowed in 0u128..=1_000_000_000,
                protocol_fees in 0u128..=100_000,
                take_rate_pct in 0u8..=100,
                ltv_pct in 0u8..=100,
                // Curve: strictly increasing utilization, non-decreasing rate
                mid_util in 1000u32..=9999,
                start_rate in 0u32..=5000,
                mid_rate_extra in 0u32..=50000,
                end_rate_extra in 0u32..=50000,
            ) -> KaminoReserve {
                let mid_rate = start_rate.saturating_add(mid_rate_extra);
                let end_rate = mid_rate.saturating_add(end_rate_extra);
                make_reserve(
                    available,
                    borrowed,
                    protocol_fees,
                    take_rate_pct,
                    ltv_pct,
                    &[(0, start_rate), (mid_util, mid_rate), (10000, end_rate)],
                )
            }
        }

        proptest! {
            #[test]
            fn utilization_never_panics(reserve in arb_reserve()) {
                let _ = calculate_kamino_utilization(&reserve);
            }

            #[test]
            fn borrow_rate_never_panics(reserve in arb_reserve()) {
                let _ = calculate_kamino_borrow_rate(&reserve);
            }

            #[test]
            fn deposit_rate_never_panics(reserve in arb_reserve()) {
                let _ = calculate_kamino_deposit_rate(&reserve);
            }

            #[test]
            fn utilization_bounded(reserve in arb_reserve()) {
                if let Ok(util) = calculate_kamino_utilization(&reserve) {
                    prop_assert!(util <= RATE_PRECISION,
                        "utilization {} > RATE_PRECISION", util);
                }
            }

            #[test]
            fn deposit_leq_borrow_when_util_bounded(reserve in arb_reserve()) {
                if let (Ok(util), Ok(deposit), Ok(borrow)) = (
                    calculate_kamino_utilization(&reserve),
                    calculate_kamino_deposit_rate(&reserve),
                    calculate_kamino_borrow_rate(&reserve),
                ) {
                    if util <= RATE_PRECISION {
                        prop_assert!(
                            deposit <= borrow,
                            "deposit {} > borrow {} (util {})",
                            deposit, borrow, util
                        );
                    }
                }
            }

            #[test]
            fn rates_non_negative(reserve in arb_reserve()) {
                if let Ok((deposit, borrow, ltv)) = compute_kamino_rates(&reserve) {
                    prop_assert!(deposit >= 0.0);
                    prop_assert!(borrow >= 0.0);
                    prop_assert!(ltv >= 0.0);
                }
            }
        }
    }
}
