use std::collections::HashMap;
use std::str::FromStr;

use rust_decimal::Decimal;

use bpx_api_types::capital::Collateral;
use bpx_api_types::futures::FuturePosition;

/// Compute margin utilization as a percentage (0–100).
/// Returns None if margin_fraction is absent (no open positions).
pub fn margin_utilization(collateral: &Collateral) -> Option<Decimal> {
    let mf = collateral.margin_fraction?;
    if mf == Decimal::ZERO {
        return Some(Decimal::ZERO);
    }
    // utilization = mmf / margin_fraction * 100
    // Higher utilization = closer to liquidation
    Some(collateral.mmf / mf * Decimal::from(100))
}

/// Information about the largest position by notional value.
pub struct LargestExposure {
    pub symbol: String,
    pub notional: Decimal,
    pub pct_of_total: Decimal,
}

/// Find the largest notional position and its share of total exposure.
pub fn largest_exposure(
    positions: &[FuturePosition],
    _mark_prices: &HashMap<String, Decimal>,
) -> Option<LargestExposure> {
    if positions.is_empty() {
        return None;
    }

    let mut max_symbol = String::new();
    let mut max_notional = Decimal::ZERO;
    let mut total_notional = Decimal::ZERO;

    for pos in positions {
        let notional = Decimal::from_str(&pos.net_exposure_notional)
            .unwrap_or(Decimal::ZERO)
            .abs();
        total_notional += notional;
        if notional > max_notional {
            max_notional = notional;
            max_symbol = pos.symbol.clone();
        }
    }

    if total_notional == Decimal::ZERO {
        return None;
    }

    Some(LargestExposure {
        symbol: max_symbol,
        notional: max_notional,
        pct_of_total: max_notional / total_notional * Decimal::from(100),
    })
}

/// Herfindahl-Hirschman Index for position concentration.
/// Range: 0 (no positions) to 10,000 (single position).
pub fn concentration_hhi(
    positions: &[FuturePosition],
    _mark_prices: &HashMap<String, Decimal>,
) -> Decimal {
    if positions.is_empty() {
        return Decimal::ZERO;
    }

    let mut total_notional = Decimal::ZERO;
    let mut notionals: Vec<Decimal> = Vec::new();

    for pos in positions {
        let notional = Decimal::from_str(&pos.net_exposure_notional)
            .unwrap_or(Decimal::ZERO)
            .abs();
        notionals.push(notional);
        total_notional += notional;
    }

    if total_notional == Decimal::ZERO {
        return Decimal::ZERO;
    }

    let mut hhi = Decimal::ZERO;
    for n in &notionals {
        let share = *n / total_notional * Decimal::from(100);
        hhi += share * share;
    }

    hhi
}

/// Classify HHI value into a human-readable label.
pub fn hhi_label(hhi: Decimal) -> &'static str {
    if hhi >= Decimal::from(5000) {
        "highly concentrated"
    } else if hhi >= Decimal::from(2500) {
        "moderate"
    } else if hhi >= Decimal::from(1500) {
        "low"
    } else {
        "diversified"
    }
}

/// Result of a stress test scenario.
pub struct StressTestResult {
    pub scenario_name: String,
    pub projected_pnl: Decimal,
    pub projected_margin_pct: Decimal,
    pub liquidation: bool,
}

/// Run a stress test: apply hypothetical price moves to positions and project the impact.
pub fn stress_test(
    positions: &[FuturePosition],
    collateral: &Collateral,
    _mark_prices: &HashMap<String, Decimal>,
    price_moves: &HashMap<String, Decimal>,
    scenario_name: &str,
) -> StressTestResult {
    let mut projected_pnl = Decimal::ZERO;

    for pos in positions {
        let net_qty = Decimal::from_str(&pos.net_quantity).unwrap_or(Decimal::ZERO);
        let mark = Decimal::from_str(&pos.mark_price).unwrap_or(Decimal::ZERO);

        // Find applicable price move
        let move_pct = price_moves
            .get(&pos.symbol)
            .copied()
            .unwrap_or(Decimal::ZERO);

        // PnL = net_quantity * mark_price * move_pct
        let position_pnl = net_qty * mark * move_pct;
        projected_pnl += position_pnl;
    }

    // Project new equity after PnL
    let new_equity = collateral.net_equity + projected_pnl;

    // Projected margin fraction: new_equity / net_exposure_futures
    let exposure = collateral.net_exposure_futures;
    let projected_margin = if exposure != Decimal::ZERO {
        (new_equity / exposure) * Decimal::from(100)
    } else {
        Decimal::from(100)
    };

    // Liquidation triggers when margin fraction drops below MMF
    let mmf_pct = collateral.mmf * Decimal::from(100);
    let liquidation = projected_margin < mmf_pct && exposure != Decimal::ZERO;

    StressTestResult {
        scenario_name: scenario_name.to_string(),
        projected_pnl,
        projected_margin_pct: projected_margin,
        liquidation,
    }
}

/// Pre-built stress test scenarios.
pub fn builtin_scenarios() -> Vec<(&'static str, HashMap<String, Decimal>)> {
    let pct = |v: i64| Decimal::new(v, 2); // e.g. -10 becomes -0.10

    vec![
        (
            "BTC -10%",
            HashMap::from([("BTC_USDC_PERP".to_string(), pct(-10))]),
        ),
        (
            "BTC -25%",
            HashMap::from([("BTC_USDC_PERP".to_string(), pct(-25))]),
        ),
        (
            "All Crypto -20%",
            HashMap::new(), // sentinel: will be expanded to all perp symbols
        ),
        (
            "Stablecoin depeg -5%",
            HashMap::new(), // sentinel: will be expanded to USDC-related
        ),
    ]
}

/// Expand "All Crypto -20%" to all position symbols.
pub fn expand_all_crypto(positions: &[FuturePosition]) -> HashMap<String, Decimal> {
    let pct = Decimal::new(-20, 2);
    positions.iter().map(|p| (p.symbol.clone(), pct)).collect()
}

/// Expand "Stablecoin depeg -5%" to USDC-related perps.
pub fn expand_stablecoin_depeg(positions: &[FuturePosition]) -> HashMap<String, Decimal> {
    let pct = Decimal::new(-5, 2);
    positions
        .iter()
        .filter(|p| p.symbol.contains("USDC"))
        .map(|p| (p.symbol.clone(), pct))
        .collect()
}

/// Parse a custom scenario string like "SOL:-15,BTC:-10" into price moves.
/// Symbol names are matched with a _USDC_PERP suffix appended.
pub fn parse_custom_scenario(input: &str) -> anyhow::Result<HashMap<String, Decimal>> {
    let mut moves = HashMap::new();
    for pair in input.split(',') {
        let pair = pair.trim();
        if pair.is_empty() {
            continue;
        }
        let parts: Vec<&str> = pair.splitn(2, ':').collect();
        if parts.len() != 2 {
            anyhow::bail!(
                "Invalid custom move format: '{pair}'. Expected SYMBOL:PCT (e.g. SOL:-15)"
            );
        }
        let symbol = parts[0].trim().to_uppercase();
        let pct_val: Decimal = parts[1]
            .trim()
            .parse()
            .map_err(|_| anyhow::anyhow!("Invalid percentage: '{}'", parts[1].trim()))?;
        let pct = pct_val / Decimal::from(100);
        // Add with _USDC_PERP suffix if not already a full symbol
        let full_symbol = if symbol.contains('_') {
            symbol
        } else {
            format!("{symbol}_USDC_PERP")
        };
        moves.insert(full_symbol, pct);
    }
    Ok(moves)
}
