//! Typed position data fetching and computation for Kamino.
//!
//! Mirrors the Drift positions module but uses Pubkey-keyed reserves
//! instead of u16 market indices.

use std::collections::HashMap;

use solana_pubkey::Pubkey;

use pyra_margin::{get_kamino_borrow_balance, get_kamino_deposit_balance};
use pyra_types::{Cache, KaminoObligation, KaminoReserve, KAMINO_FRACTION_SCALE};

use crate::{RedisClient, RedisError, RedisKey, RedisResult};

/// Extract market price (f64) from a Kamino reserve's `market_price_sf` field.
///
/// Divides by 2^60 (Kamino Fraction type).
fn market_price_from_reserve(reserve: &KaminoReserve) -> f64 {
    reserve.liquidity.market_price_sf as f64 / KAMINO_FRACTION_SCALE as f64
}

/// Convert a token balance (i128) to a signed USD value in cents using the given price and decimals.
///
/// Uses f64 floating-point arithmetic for display and caching purposes only.
/// The authoritative balance-to-USD conversion for spending decisions lives in
/// `pyra_margin::kamino::balance` / `pyra_margin::kamino::capacity`, which uses
/// checked integer arithmetic (u128/i128) to avoid rounding errors.
fn balance_to_value_cents(token_balance: i128, decimals: u32, price: f64) -> RedisResult<i64> {
    let decimals_pow = 10_f64.powi(i32::try_from(decimals).map_err(|_| RedisError::MathOverflow)?);
    let value = (token_balance as f64) / decimals_pow * price * 100.0;
    let rounded = value.round();
    if rounded.is_finite() && rounded >= i64::MIN as f64 && rounded <= i64::MAX as f64 {
        Ok(rounded as i64)
    } else {
        Err(RedisError::MathOverflow)
    }
}

// ── Data structures ──────────────────────────────────────────────────

/// Parsed Redis position data for a single vault's Kamino obligation.
pub struct VaultKaminoPositionData {
    /// The cached KaminoObligation account (includes slot information).
    pub obligation: Cache<KaminoObligation>,
    /// Kamino reserves keyed by reserve pubkey.
    pub reserves: HashMap<Pubkey, KaminoReserve>,
    /// Market prices (USD) keyed by reserve pubkey, extracted from reserve data.
    pub prices: HashMap<Pubkey, f64>,
}

/// All Kamino obligation positions with shared reserve/price data.
pub struct AllKaminoPositionsData {
    /// `(vault_address, obligation_cache)` pairs for every Kamino obligation in Redis.
    pub obligations: Vec<(Pubkey, Cache<KaminoObligation>)>,
    /// Shared reserves keyed by reserve pubkey.
    pub reserves: HashMap<Pubkey, KaminoReserve>,
    /// Market prices (USD) keyed by reserve pubkey.
    pub prices: HashMap<Pubkey, f64>,
}

// ── Fetch methods on RedisClient ─────────────────────────────────────

impl RedisClient {
    /// Fetch KaminoObligation, reserves, and prices for a vault via MGET.
    ///
    /// Builds keys as: `[obligation, reserve_0..N]` and extracts prices
    /// from `reserve.liquidity.market_price_sf`.
    ///
    /// Returns `NotFound` if the obligation key is missing.
    pub async fn fetch_vault_kamino_position_data(
        &self,
        vault_address: &Pubkey,
        lending_market: &Pubkey,
        reserve_pubkeys: &[Pubkey],
    ) -> RedisResult<VaultKaminoPositionData> {
        let obligation_key = RedisKey::kamino_obligation(vault_address, lending_market).to_string();
        let mut keys = vec![obligation_key];
        for pk in reserve_pubkeys {
            keys.push(RedisKey::kamino_reserve(pk).to_string());
        }

        let values = self.mget(&keys).await?;

        let obligation_raw = values
            .first()
            .and_then(|v| v.as_ref())
            .ok_or_else(|| RedisError::NotFound("KaminoObligation not found in Redis".into()))?;
        let obligation: Cache<KaminoObligation> = serde_json::from_str(obligation_raw)?;

        let mut reserves: HashMap<Pubkey, KaminoReserve> = HashMap::new();
        let mut prices: HashMap<Pubkey, f64> = HashMap::new();

        for (i, pk) in reserve_pubkeys.iter().enumerate() {
            if let Some(Some(raw)) =
                values.get(1usize.checked_add(i).ok_or(RedisError::MathOverflow)?)
            {
                if let Ok(cached) = serde_json::from_str::<Cache<KaminoReserve>>(raw) {
                    let price = market_price_from_reserve(&cached.account);
                    prices.insert(*pk, price);
                    reserves.insert(*pk, cached.account);
                }
            }
        }

        Ok(VaultKaminoPositionData {
            obligation,
            reserves,
            prices,
        })
    }

    /// Fetch ALL Kamino obligations from Redis via SCAN + MGET.
    ///
    /// Scans every `account:kamino:obligation:*` key and fetches all
    /// obligation data in a single MGET round-trip.
    pub async fn fetch_all_kamino_positions(
        &self,
        reserve_pubkeys: &[Pubkey],
    ) -> RedisResult<AllKaminoPositionsData> {
        // 1. Scan for all obligation keys
        let obligation_keys = self.scan_keys(&RedisKey::kamino_obligation_glob()).await?;

        let prefix_with_colon = format!("{}:", RedisKey::KAMINO_OBLIGATION_PREFIX);
        let vault_addresses: Vec<Option<Pubkey>> = obligation_keys
            .iter()
            .map(|k| {
                k.strip_prefix(prefix_with_colon.as_str())
                    .and_then(|rest| rest.split(':').next())
                    .and_then(|s| s.parse::<Pubkey>().ok())
            })
            .collect();

        // 2. Build MGET: [obligations...] [reserves...]
        let num_obligations = obligation_keys.len();
        let mut all_keys: Vec<String> = obligation_keys;

        for pk in reserve_pubkeys {
            all_keys.push(RedisKey::kamino_reserve(pk).to_string());
        }

        let values = self.mget(&all_keys).await?;

        // 3. Parse obligations
        let mut obligations: Vec<(Pubkey, Cache<KaminoObligation>)> = Vec::new();
        for (i, vault_pk) in vault_addresses.iter().enumerate() {
            if let (Some(pk), Some(Some(raw))) = (vault_pk, values.get(i)) {
                if let Ok(cached) = serde_json::from_str::<Cache<KaminoObligation>>(raw) {
                    obligations.push((*pk, cached));
                }
            }
        }

        // 4. Parse reserves and extract prices
        let mut reserves: HashMap<Pubkey, KaminoReserve> = HashMap::new();
        let mut prices: HashMap<Pubkey, f64> = HashMap::new();

        for (i, pk) in reserve_pubkeys.iter().enumerate() {
            let offset = num_obligations
                .checked_add(i)
                .ok_or(RedisError::MathOverflow)?;
            if let Some(Some(raw)) = values.get(offset) {
                if let Ok(cached) = serde_json::from_str::<Cache<KaminoReserve>>(raw) {
                    let price = market_price_from_reserve(&cached.account);
                    prices.insert(*pk, price);
                    reserves.insert(*pk, cached.account);
                }
            }
        }

        Ok(AllKaminoPositionsData {
            obligations,
            reserves,
            prices,
        })
    }
}

// ── Computation helpers ──────────────────────────────────────────────

/// Compute the signed USD-cent value for each position in a Kamino obligation.
///
/// Deposits are positive, borrows are negative.
pub fn compute_kamino_position_values(data: &VaultKaminoPositionData) -> RedisResult<Vec<i64>> {
    compute_user_kamino_position_values(&data.obligation.account, &data.reserves, &data.prices)
}

/// Compute per-asset data: `(reserve_pubkey, balance_base_units, value_cents)`.
pub fn compute_kamino_asset_data(
    data: &VaultKaminoPositionData,
) -> RedisResult<Vec<(Pubkey, i64, i64)>> {
    compute_user_kamino_asset_data(&data.obligation.account, &data.reserves, &data.prices)
}

/// Compute signed USD-cent position values for a single Kamino obligation,
/// using shared reserve and price maps.
pub fn compute_user_kamino_position_values(
    obligation: &KaminoObligation,
    reserves: &HashMap<Pubkey, KaminoReserve>,
    prices: &HashMap<Pubkey, f64>,
) -> RedisResult<Vec<i64>> {
    let mut results = Vec::new();

    for deposit in &obligation.deposits {
        if deposit.deposited_amount == 0 {
            continue;
        }
        let Some(reserve) = reserves.get(&deposit.deposit_reserve) else {
            continue;
        };
        let Some(&price) = prices.get(&deposit.deposit_reserve) else {
            continue;
        };
        let balance = get_kamino_deposit_balance(deposit, reserve)?;
        let decimals =
            u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
        let cents = balance_to_value_cents(balance, decimals, price)?;
        results.push(cents);
    }

    for borrow in &obligation.borrows {
        if borrow.borrowed_amount_sf == 0 {
            continue;
        }
        let Some(reserve) = reserves.get(&borrow.borrow_reserve) else {
            continue;
        };
        let Some(&price) = prices.get(&borrow.borrow_reserve) else {
            continue;
        };
        let balance = get_kamino_borrow_balance(borrow, reserve)?;
        let decimals =
            u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
        let cents = balance_to_value_cents(balance, decimals, price)?;
        results.push(cents);
    }

    Ok(results)
}

/// Compute per-asset data for a single Kamino obligation using shared reserve/price maps.
///
/// Returns `(reserve_pubkey, signed_token_balance_i64, signed_value_cents)` tuples.
pub fn compute_user_kamino_asset_data(
    obligation: &KaminoObligation,
    reserves: &HashMap<Pubkey, KaminoReserve>,
    prices: &HashMap<Pubkey, f64>,
) -> RedisResult<Vec<(Pubkey, i64, i64)>> {
    let mut results = Vec::new();

    for deposit in &obligation.deposits {
        if deposit.deposited_amount == 0 {
            continue;
        }
        let Some(reserve) = reserves.get(&deposit.deposit_reserve) else {
            continue;
        };
        let Some(&price) = prices.get(&deposit.deposit_reserve) else {
            continue;
        };
        let balance_i128 = get_kamino_deposit_balance(deposit, reserve)?;
        let balance = i64::try_from(balance_i128).map_err(|_| RedisError::MathOverflow)?;
        let decimals =
            u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
        let cents = balance_to_value_cents(balance_i128, decimals, price)?;
        results.push((deposit.deposit_reserve, balance, cents));
    }

    for borrow in &obligation.borrows {
        if borrow.borrowed_amount_sf == 0 {
            continue;
        }
        let Some(reserve) = reserves.get(&borrow.borrow_reserve) else {
            continue;
        };
        let Some(&price) = prices.get(&borrow.borrow_reserve) else {
            continue;
        };
        let balance_i128 = get_kamino_borrow_balance(borrow, reserve)?;
        let balance = i64::try_from(balance_i128).map_err(|_| RedisError::MathOverflow)?;
        let decimals =
            u32::try_from(reserve.liquidity.mint_decimals).map_err(|_| RedisError::MathOverflow)?;
        let cents = balance_to_value_cents(balance_i128, decimals, price)?;
        results.push((borrow.borrow_reserve, balance, cents));
    }

    Ok(results)
}

#[cfg(test)]
#[allow(
    clippy::allow_attributes,
    clippy::allow_attributes_without_reason,
    clippy::unwrap_used,
    clippy::expect_used,
    clippy::panic,
    clippy::arithmetic_side_effects,
    reason = "test code"
)]
mod tests {
    use super::*;
    use pyra_types::{
        KaminoBigFractionBytes, KaminoBorrowRateCurve, KaminoCurvePoint, KaminoLastUpdate,
        KaminoObligationCollateral, KaminoObligationLiquidity, KaminoReserveCollateral,
        KaminoReserveConfig, KaminoReserveFees, KaminoReserveLiquidity, KaminoWithdrawalCaps,
    };
    fn test_pubkey(seed: u8) -> Pubkey {
        Pubkey::new_from_array([seed; 32])
    }

    const FRACTION_ONE: u128 = 1 << 60;

    fn rate_to_bsf(rate: u128) -> KaminoBigFractionBytes {
        KaminoBigFractionBytes {
            value: [rate as u64, (rate >> 64) as u64, 0, 0],
        }
    }

    fn make_reserve(mint_decimals: u64, market_price_sf: u128) -> KaminoReserve {
        KaminoReserve {
            lending_market: Pubkey::default(),
            liquidity: KaminoReserveLiquidity {
                mint_pubkey: Pubkey::default(),
                supply_vault: Pubkey::default(),
                fee_vault: Pubkey::default(),
                total_available_amount: 1_000_000,
                borrowed_amount_sf: 0,
                cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
                mint_decimals,
                market_price_sf,
                accumulated_protocol_fees_sf: 0,
                accumulated_referrer_fees_sf: 0,
                pending_referrer_fees_sf: 0,
                token_program: Pubkey::default(),
            },
            collateral: KaminoReserveCollateral {
                mint_pubkey: Pubkey::default(),
                supply_vault: Pubkey::default(),
                mint_total_supply: 1_000_000,
            },
            config: KaminoReserveConfig {
                loan_to_value_pct: 80,
                liquidation_threshold_pct: 85,
                protocol_take_rate_pct: 10,
                protocol_liquidation_fee_pct: 5,
                borrow_factor_pct: 100,
                deposit_limit: u64::MAX,
                borrow_limit: u64::MAX,
                fees: KaminoReserveFees {
                    origination_fee_sf: 0,
                    flash_loan_fee_sf: 0,
                },
                borrow_rate_curve: KaminoBorrowRateCurve {
                    points: [KaminoCurvePoint {
                        utilization_rate_bps: 0,
                        borrow_rate_bps: 0,
                    }; 11],
                },
                deposit_withdrawal_cap: KaminoWithdrawalCaps {
                    config_capacity: 0,
                    current_total: 0,
                    last_interval_start_timestamp: 0,
                    config_interval_length_seconds: 0,
                },
                debt_withdrawal_cap: KaminoWithdrawalCaps {
                    config_capacity: 0,
                    current_total: 0,
                    last_interval_start_timestamp: 0,
                    config_interval_length_seconds: 0,
                },
                elevation_groups: [0; 20],
            },
            last_update: KaminoLastUpdate { slot: 0, stale: 0 },
        }
    }

    fn make_obligation(
        deposits: Vec<KaminoObligationCollateral>,
        borrows: Vec<KaminoObligationLiquidity>,
    ) -> KaminoObligation {
        KaminoObligation {
            lending_market: Pubkey::default(),
            owner: Pubkey::default(),
            deposits,
            borrows,
            deposited_value_sf: 0,
            borrowed_assets_market_value_sf: 0,
            allowed_borrow_value_sf: 0,
            unhealthy_borrow_value_sf: 0,
            has_debt: 0,
            elevation_group: 0,
            last_update: KaminoLastUpdate { slot: 0, stale: 0 },
        }
    }

    #[test]
    fn market_price_extraction() {
        // 1.0 USD = 1 * 2^60
        let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
        let price = market_price_from_reserve(&reserve);
        assert!((price - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn market_price_extraction_150_usd() {
        // 150.0 USD = 150 * 2^60
        let price_sf = KAMINO_FRACTION_SCALE
            .checked_mul(150)
            .expect("test value fits");
        let reserve = make_reserve(9, price_sf);
        let price = market_price_from_reserve(&reserve);
        assert!((price - 150.0).abs() < 0.001);
    }

    #[test]
    fn vault_position_data_struct_construction() {
        let reserve_pk = test_pubkey(1);
        let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);

        let mut reserves = HashMap::new();
        reserves.insert(reserve_pk, reserve);
        let mut prices = HashMap::new();
        prices.insert(reserve_pk, 1.0);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: reserve_pk,
            deposited_amount: 1_000_000,
            market_value_sf: KAMINO_FRACTION_SCALE,
        };
        let obligation = make_obligation(vec![deposit], vec![]);

        let data = VaultKaminoPositionData {
            obligation: Cache {
                account: obligation,
                last_updated_slot: 12345,
            },
            reserves,
            prices,
        };

        assert_eq!(data.obligation.last_updated_slot, 12345);
        assert_eq!(data.reserves.len(), 1);
        assert_eq!(data.prices.len(), 1);
    }

    #[test]
    fn all_positions_data_struct_construction() {
        let data = AllKaminoPositionsData {
            obligations: vec![],
            reserves: HashMap::new(),
            prices: HashMap::new(),
        };
        assert!(data.obligations.is_empty());
        assert!(data.reserves.is_empty());
        assert!(data.prices.is_empty());
    }

    #[test]
    fn compute_values_empty_obligation() {
        let obligation = make_obligation(vec![], vec![]);
        let data = VaultKaminoPositionData {
            obligation: Cache {
                account: obligation,
                last_updated_slot: 0,
            },
            reserves: HashMap::new(),
            prices: HashMap::new(),
        };
        let values = compute_kamino_position_values(&data).unwrap();
        assert!(values.is_empty());
    }

    #[test]
    fn compute_values_single_deposit() {
        let reserve_pk = test_pubkey(1);
        // 1:1 exchange rate, 6 decimals, $1.00
        let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: reserve_pk,
            deposited_amount: 1_000_000, // 1 token
            market_value_sf: 0,
        };
        let obligation = make_obligation(vec![deposit], vec![]);

        let mut reserves = HashMap::new();
        reserves.insert(reserve_pk, reserve);
        let mut prices = HashMap::new();
        prices.insert(reserve_pk, 1.0);

        let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
        assert_eq!(values.len(), 1);
        assert_eq!(values[0], 100); // 1 token * $1.00 = 100 cents
    }

    #[test]
    fn compute_values_deposit_and_borrow() {
        let deposit_pk = test_pubkey(1);
        let borrow_pk = test_pubkey(2);

        let deposit_reserve = make_reserve(6, KAMINO_FRACTION_SCALE);
        let borrow_reserve = make_reserve(6, KAMINO_FRACTION_SCALE);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: deposit_pk,
            deposited_amount: 2_000_000,
            market_value_sf: 0,
        };
        let borrow = KaminoObligationLiquidity {
            borrow_reserve: borrow_pk,
            cumulative_borrow_rate_bsf: rate_to_bsf(FRACTION_ONE),
            borrowed_amount_sf: 500_000 * FRACTION_ONE,
            market_value_sf: 0,
            borrow_factor_adjusted_market_value_sf: 0,
        };
        let obligation = make_obligation(vec![deposit], vec![borrow]);

        let mut reserves = HashMap::new();
        reserves.insert(deposit_pk, deposit_reserve);
        reserves.insert(borrow_pk, borrow_reserve);
        let mut prices = HashMap::new();
        prices.insert(deposit_pk, 1.0);
        prices.insert(borrow_pk, 1.0);

        let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
        assert_eq!(values.len(), 2);
        assert_eq!(values[0], 200); // 2 tokens deposited = 200 cents
        assert_eq!(values[1], -50); // 0.5 tokens borrowed = -50 cents
    }

    #[test]
    fn compute_values_skips_zero_deposit() {
        let reserve_pk = test_pubkey(1);
        let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: reserve_pk,
            deposited_amount: 0,
            market_value_sf: 0,
        };
        let obligation = make_obligation(vec![deposit], vec![]);

        let mut reserves = HashMap::new();
        reserves.insert(reserve_pk, reserve);
        let mut prices = HashMap::new();
        prices.insert(reserve_pk, 1.0);

        let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
        assert!(values.is_empty());
    }

    #[test]
    fn compute_values_skips_missing_reserve() {
        let reserve_pk = test_pubkey(1);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: reserve_pk,
            deposited_amount: 1_000_000,
            market_value_sf: 0,
        };
        let obligation = make_obligation(vec![deposit], vec![]);

        let reserves = HashMap::new();
        let mut prices = HashMap::new();
        prices.insert(reserve_pk, 1.0);

        let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
        assert!(values.is_empty());
    }

    #[test]
    fn compute_values_skips_missing_price() {
        let reserve_pk = test_pubkey(1);
        let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: reserve_pk,
            deposited_amount: 1_000_000,
            market_value_sf: 0,
        };
        let obligation = make_obligation(vec![deposit], vec![]);

        let mut reserves = HashMap::new();
        reserves.insert(reserve_pk, reserve);
        let prices = HashMap::new();

        let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
        assert!(values.is_empty());
    }

    #[test]
    fn compute_values_high_price_sol() {
        let reserve_pk = test_pubkey(1);
        // SOL: 9 decimals, $150
        let price_sf = KAMINO_FRACTION_SCALE.checked_mul(150).unwrap();
        let reserve = make_reserve(9, price_sf);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: reserve_pk,
            deposited_amount: 100_000_000, // 0.1 SOL
            market_value_sf: 0,
        };
        let obligation = make_obligation(vec![deposit], vec![]);

        let mut reserves = HashMap::new();
        reserves.insert(reserve_pk, reserve);
        let mut prices = HashMap::new();
        prices.insert(reserve_pk, 150.0);

        let values = compute_user_kamino_position_values(&obligation, &reserves, &prices).unwrap();
        assert_eq!(values.len(), 1);
        assert_eq!(values[0], 1500); // 0.1 SOL * $150 = $15 = 1500 cents
    }

    #[test]
    fn compute_asset_data_returns_tuples() {
        let reserve_pk = test_pubkey(1);
        let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: reserve_pk,
            deposited_amount: 5_000_000,
            market_value_sf: 0,
        };
        let obligation = make_obligation(vec![deposit], vec![]);

        let mut reserves = HashMap::new();
        reserves.insert(reserve_pk, reserve);
        let mut prices = HashMap::new();
        prices.insert(reserve_pk, 1.0);

        let data = compute_user_kamino_asset_data(&obligation, &reserves, &prices).unwrap();
        assert_eq!(data.len(), 1);
        let (pk, token_balance, value_cents) = data[0];
        assert_eq!(pk, reserve_pk);
        assert_eq!(token_balance, 5_000_000);
        assert_eq!(value_cents, 500); // 5 USDC = 500 cents
    }

    #[test]
    fn compute_position_values_delegates_to_user_variant() {
        let reserve_pk = test_pubkey(1);
        let reserve = make_reserve(6, KAMINO_FRACTION_SCALE);

        let deposit = KaminoObligationCollateral {
            deposit_reserve: reserve_pk,
            deposited_amount: 1_000_000,
            market_value_sf: 0,
        };
        let obligation = Cache {
            account: make_obligation(vec![deposit], vec![]),
            last_updated_slot: 12345,
        };

        let mut reserves = HashMap::new();
        reserves.insert(reserve_pk, reserve);
        let mut prices = HashMap::new();
        prices.insert(reserve_pk, 1.0);

        let vpd = VaultKaminoPositionData {
            obligation,
            reserves,
            prices,
        };
        let values = compute_kamino_position_values(&vpd).unwrap();
        assert_eq!(values.len(), 1);
        assert_eq!(values[0], 100);
    }
}
