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

use anyhow::Result;
use clap::Subcommand;
use rust_decimal::Decimal;

use crate::client;
use crate::config::ResolvedConfig;
use crate::output::scenario as out;
use crate::output::OutputFormat;
use crate::retry::{self, RetryConfig};

#[derive(Debug, Subcommand)]
pub enum ScenarioCommand {
    /// Simulate adding a trade and see margin/risk impact
    Trade {
        /// Market symbol (e.g. SOL_USDC_PERP)
        symbol: String,
        /// Trade side (buy or sell)
        side: String,
        /// Trade quantity
        quantity: String,
        /// Limit price (defaults to current mark price)
        #[arg(long)]
        price: Option<String>,
    },

    /// Simulate a percentage price move and see portfolio impact
    PriceMove {
        /// Price move percentage (e.g. -10, +5, 20)
        percent: String,
        /// Apply move only to this symbol (default: all positions)
        #[arg(long)]
        symbol: Option<String>,
    },
}

/// Snapshot of account risk metrics for comparison.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ScenarioMetrics {
    pub margin_fraction: Option<Decimal>,
    pub net_exposure: Decimal,
    pub unrealized_pnl: Decimal,
    pub net_equity: Decimal,
    pub liquidation_distance_pct: Option<Decimal>,
}

/// Full result of a scenario simulation.
#[derive(Debug, Clone, serde::Serialize)]
pub struct ScenarioResult {
    pub label: String,
    pub current: ScenarioMetrics,
    pub projected: ScenarioMetrics,
}

pub async fn run(
    cmd: &ScenarioCommand,
    config: &ResolvedConfig,
    secret: Option<&str>,
    base_url: Option<&str>,
    format: OutputFormat,
    retry_config: &RetryConfig,
) -> Result<()> {
    let c = client::authenticated_client(config, secret, base_url)?;

    match cmd {
        ScenarioCommand::Trade {
            symbol,
            side,
            quantity,
            price,
        } => {
            let symbol = symbol.to_uppercase();
            let side_upper = side.to_uppercase();
            if side_upper != "BUY" && side_upper != "SELL" {
                anyhow::bail!("Side must be 'buy' or 'sell', got '{side}'");
            }
            let qty = Decimal::from_str(quantity)
                .map_err(|_| anyhow::anyhow!("Invalid quantity: {quantity}"))?;
            if qty <= Decimal::ZERO {
                anyhow::bail!("Quantity must be positive");
            }

            let sp = client::spinner_for("Fetching account data...", format);

            let json_mode = format == OutputFormat::Json;
            let c2 = c.clone();
            let c3 = c.clone();

            let (collateral_res, positions_res, mark_prices_res) = tokio::join!(
                retry::with_retry(retry_config, json_mode, || {
                    let c = c.clone();
                    async move { c.get_collateral().await.map_err(|e| anyhow::anyhow!("{e}")) }
                }),
                retry::with_retry(retry_config, json_mode, || {
                    let c = c2.clone();
                    async move {
                        c.get_open_future_positions()
                            .await
                            .map_err(|e| anyhow::anyhow!("{e}"))
                    }
                }),
                retry::with_retry(retry_config, json_mode, || {
                    let c = c3.clone();
                    async move {
                        c.get_all_mark_prices()
                            .await
                            .map_err(|e| anyhow::anyhow!("{e}"))
                    }
                }),
            );

            sp.finish_and_clear();

            let collateral = collateral_res?;
            let positions = positions_res?;
            let mark_prices = mark_prices_res?;

            // Build mark price lookup
            let price_map: HashMap<String, Decimal> = mark_prices
                .iter()
                .map(|mp| (mp.symbol.clone(), mp.mark_price))
                .collect();

            // Determine trade price
            let trade_price = if let Some(p) = price {
                Decimal::from_str(p).map_err(|_| anyhow::anyhow!("Invalid price: {p}"))?
            } else {
                *price_map
                    .get(&symbol)
                    .ok_or_else(|| anyhow::anyhow!("No mark price found for {symbol}"))?
            };

            let trade_notional = qty * trade_price;

            // Current metrics
            let current_exposure = collateral.net_exposure_futures.abs();
            let current = ScenarioMetrics {
                margin_fraction: collateral.margin_fraction,
                net_exposure: current_exposure,
                unrealized_pnl: collateral.pnl_unrealized,
                net_equity: collateral.net_equity,
                liquidation_distance_pct: compute_liq_distance(
                    collateral.margin_fraction,
                    collateral.mmf,
                ),
            };

            // Projected metrics: estimate new exposure
            // Find existing position for this symbol to determine net effect
            let existing_notional = positions
                .iter()
                .find(|p| p.symbol == symbol)
                .and_then(|p| Decimal::from_str(&p.net_exposure_notional).ok())
                .unwrap_or(Decimal::ZERO);

            let signed_add = if side_upper == "BUY" {
                trade_notional
            } else {
                -trade_notional
            };
            let new_position_notional = existing_notional + signed_add;

            // Recompute total exposure: subtract old position, add new
            let other_exposure = current_exposure - existing_notional.abs();
            let projected_exposure = other_exposure + new_position_notional.abs();

            // Estimate projected margin fraction: MF = net_equity / total_exposure
            let projected_mf = if projected_exposure > Decimal::ZERO {
                Some(collateral.net_equity / projected_exposure)
            } else {
                None
            };

            let projected_liq_dist = compute_liq_distance(projected_mf, collateral.mmf);

            let projected = ScenarioMetrics {
                margin_fraction: projected_mf,
                net_exposure: projected_exposure,
                unrealized_pnl: collateral.pnl_unrealized, // PnL unchanged at trade time
                net_equity: collateral.net_equity,
                liquidation_distance_pct: projected_liq_dist,
            };

            let label = format!("{} {} {} @ {}", side_upper, qty, symbol, trade_price);

            let result = ScenarioResult {
                label,
                current,
                projected,
            };

            out::print_scenario(&result, format);
        }

        ScenarioCommand::PriceMove { percent, symbol } => {
            let pct = Decimal::from_str(percent.trim_start_matches('+'))
                .map_err(|_| anyhow::anyhow!("Invalid percentage: {percent}"))?;
            let pct_factor = Decimal::ONE + pct / Decimal::from(100);

            let sp = client::spinner_for("Fetching account data...", format);

            let json_mode = format == OutputFormat::Json;
            let c2 = c.clone();
            let c3 = c.clone();

            let (collateral_res, positions_res, mark_prices_res) = tokio::join!(
                retry::with_retry(retry_config, json_mode, || {
                    let c = c.clone();
                    async move { c.get_collateral().await.map_err(|e| anyhow::anyhow!("{e}")) }
                }),
                retry::with_retry(retry_config, json_mode, || {
                    let c = c2.clone();
                    async move {
                        c.get_open_future_positions()
                            .await
                            .map_err(|e| anyhow::anyhow!("{e}"))
                    }
                }),
                retry::with_retry(retry_config, json_mode, || {
                    let c = c3.clone();
                    async move {
                        c.get_all_mark_prices()
                            .await
                            .map_err(|e| anyhow::anyhow!("{e}"))
                    }
                }),
            );

            sp.finish_and_clear();

            let collateral = collateral_res?;
            let positions = positions_res?;
            let mark_prices = mark_prices_res?;

            let _mark_prices = mark_prices;

            let filter_symbol = symbol.as_ref().map(|s| s.to_uppercase());

            // Current metrics
            let current_exposure = collateral.net_exposure_futures.abs();
            let current = ScenarioMetrics {
                margin_fraction: collateral.margin_fraction,
                net_exposure: current_exposure,
                unrealized_pnl: collateral.pnl_unrealized,
                net_equity: collateral.net_equity,
                liquidation_distance_pct: compute_liq_distance(
                    collateral.margin_fraction,
                    collateral.mmf,
                ),
            };

            // Project PnL change from price move on positions
            let mut pnl_delta = Decimal::ZERO;
            let mut projected_exposure = 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);

                let applies = match &filter_symbol {
                    Some(sym) => pos.symbol == *sym,
                    None => true,
                };

                if applies {
                    let new_mark = mark * pct_factor;
                    let price_change = new_mark - mark;
                    pnl_delta += net_qty * price_change;
                    projected_exposure += (net_qty * new_mark).abs();
                } else {
                    projected_exposure += (net_qty * mark).abs();
                }
            }

            // Also adjust collateral asset values for the price move
            let mut equity_delta = pnl_delta;
            for item in &collateral.collateral {
                let applies = match &filter_symbol {
                    Some(sym) => item.symbol == *sym,
                    None => true,
                };
                if applies && !item.symbol.contains("USD") {
                    let notional_change = item.balance_notional * (pct_factor - Decimal::ONE);
                    equity_delta += notional_change;
                }
            }

            let projected_equity = collateral.net_equity + equity_delta;
            let projected_pnl = collateral.pnl_unrealized + pnl_delta;

            let projected_mf = if projected_exposure > Decimal::ZERO {
                Some(projected_equity / projected_exposure)
            } else {
                None
            };

            let projected_liq_dist = compute_liq_distance(projected_mf, collateral.mmf);

            let projected = ScenarioMetrics {
                margin_fraction: projected_mf,
                net_exposure: projected_exposure,
                unrealized_pnl: projected_pnl,
                net_equity: projected_equity,
                liquidation_distance_pct: projected_liq_dist,
            };

            let label = if let Some(sym) = &filter_symbol {
                format!(
                    "{}{pct}% price move on {sym}",
                    if pct >= Decimal::ZERO { "+" } else { "" }
                )
            } else {
                format!(
                    "{}{pct}% price move (all positions)",
                    if pct >= Decimal::ZERO { "+" } else { "" }
                )
            };

            let result = ScenarioResult {
                label,
                current,
                projected,
            };

            out::print_scenario(&result, format);
        }
    }

    Ok(())
}

/// Compute liquidation distance as (MF - MMF) / MF * 100.
/// Returns None if margin fraction is zero or unavailable.
fn compute_liq_distance(margin_fraction: Option<Decimal>, mmf: Decimal) -> Option<Decimal> {
    let mf = margin_fraction?;
    if mf == Decimal::ZERO {
        return None;
    }
    Some((mf - mmf) / mf * Decimal::from(100))
}
