use anyhow::Result;
use clap::Args;
use rust_decimal::Decimal;
use serde::Serialize;
use std::str::FromStr;

use bpx_api_types::order::Order;

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

#[derive(Debug, Args)]
pub struct PortfolioArgs {
    /// Show full collateral breakdown
    #[arg(long)]
    pub detail: bool,

    /// Include lending positions
    #[arg(long)]
    pub lending: bool,
}

#[derive(Debug, Serialize)]
pub struct PortfolioSnapshot {
    pub timestamp: String,
    pub account_health: AccountHealth,
    pub pnl: PnlSummary,
    pub collateral: CollateralSummary,
    pub positions: Vec<PositionEntry>,
    pub open_orders: OpenOrdersSummary,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub lending: Option<LendingSummary>,
}

#[derive(Debug, Serialize)]
pub struct AccountHealth {
    pub net_equity: Decimal,
    pub margin_fraction: Option<Decimal>,
    pub margin_utilization_pct: Option<Decimal>,
    pub distance_to_liquidation_pct: Option<Decimal>,
}

#[derive(Debug, Serialize)]
pub struct PnlSummary {
    pub unrealized_total: Decimal,
    pub funding_accrued: Decimal,
}

#[derive(Debug, Serialize)]
pub struct CollateralSummary {
    pub total_value: Decimal,
    pub weighted_value: Decimal,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub breakdown: Option<Vec<CollateralItem>>,
}

#[derive(Debug, Serialize)]
pub struct CollateralItem {
    pub symbol: String,
    pub notional: Decimal,
    pub weight: Decimal,
    pub value: Decimal,
}

#[derive(Debug, Serialize)]
pub struct PositionEntry {
    pub symbol: String,
    pub net_quantity: String,
    pub entry_price: String,
    pub mark_price: String,
    pub unrealized_pnl: String,
    pub liquidation_price: String,
}

#[derive(Debug, Serialize)]
pub struct OpenOrdersSummary {
    pub count: usize,
    pub notional_locked: Decimal,
}

#[derive(Debug, Serialize)]
pub struct LendingSummary {
    pub total_lent_value: Decimal,
    pub daily_income: Decimal,
}

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

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

    let (collateral_res, positions_res, balances_res, orders_res, lending_res, mark_prices_res) = tokio::join!(
        retry::with_retry(retry_config, json_mode, || {
            let client = &c;
            async move {
                client
                    .get_collateral()
                    .await
                    .map_err(|e| anyhow::anyhow!("{e}"))
            }
        }),
        retry::with_retry(retry_config, json_mode, || {
            let client = &c;
            async move {
                client
                    .get_open_future_positions()
                    .await
                    .map_err(|e| anyhow::anyhow!("{e}"))
            }
        }),
        retry::with_retry(retry_config, json_mode, || {
            let client = &c;
            async move {
                client
                    .get_balances()
                    .await
                    .map_err(|e| anyhow::anyhow!("{e}"))
            }
        }),
        retry::with_retry(retry_config, json_mode, || {
            let client = &c;
            async move {
                client
                    .get_open_orders(None)
                    .await
                    .map_err(|e| anyhow::anyhow!("{e}"))
            }
        }),
        retry::with_retry(retry_config, json_mode, || {
            let client = &c;
            async move {
                client
                    .get_borrow_lend_positions()
                    .await
                    .map_err(|e| anyhow::anyhow!("{e}"))
            }
        }),
        retry::with_retry(retry_config, json_mode, || {
            let client = &c;
            async move {
                client
                    .get_all_mark_prices()
                    .await
                    .map_err(|e| anyhow::anyhow!("{e}"))
            }
        }),
    );

    sp.finish_and_clear();

    let collateral = collateral_res?;
    let positions = positions_res?;
    let _balances = balances_res?;
    let orders = orders_res?;
    let borrow_lend = lending_res?;
    let mark_prices = mark_prices_res?;

    // Build a lookup from symbol -> mark_price
    let mark_price_map: std::collections::HashMap<String, Decimal> = mark_prices
        .iter()
        .map(|p| (p.symbol.clone(), p.mark_price))
        .collect();

    // Account health
    let mf = collateral.margin_fraction;
    let margin_util_pct = mf.map(|f| f * Decimal::from(100));
    let distance_to_liq_pct = mf.and_then(|f| {
        if f < Decimal::ONE {
            Some((Decimal::ONE - f) * Decimal::from(100))
        } else {
            None
        }
    });

    let account_health = AccountHealth {
        net_equity: collateral.net_equity,
        margin_fraction: mf,
        margin_utilization_pct: margin_util_pct,
        distance_to_liquidation_pct: distance_to_liq_pct,
    };

    // PnL
    let funding_accrued: Decimal = positions
        .iter()
        .filter_map(|p| Decimal::from_str(&p.pnl_realized).ok())
        .sum();

    let pnl = PnlSummary {
        unrealized_total: collateral.pnl_unrealized,
        funding_accrued,
    };

    // Collateral summary
    let breakdown = if args.detail {
        Some(
            collateral
                .collateral
                .iter()
                .map(|c| CollateralItem {
                    symbol: c.symbol.clone(),
                    notional: c.balance_notional,
                    weight: c.collateral_weight,
                    value: c.collateral_value,
                })
                .collect(),
        )
    } else {
        None
    };

    let collateral_summary = CollateralSummary {
        total_value: collateral.assets_value,
        weighted_value: collateral.net_equity,
        breakdown,
    };

    // Positions
    let position_entries: Vec<PositionEntry> = positions
        .iter()
        .map(|p| PositionEntry {
            symbol: p.symbol.clone(),
            net_quantity: p.net_quantity.clone(),
            entry_price: p.entry_price.clone(),
            mark_price: p.mark_price.clone(),
            unrealized_pnl: p.pnl_unrealized.clone(),
            liquidation_price: p.est_liquidation_price.clone(),
        })
        .collect();

    // Open orders notional
    let notional_locked: Decimal = orders
        .iter()
        .map(|o| match o {
            Order::Limit(l) => l.quantity * l.price,
            Order::Market(m) => {
                let qty = m.quantity.unwrap_or(Decimal::ZERO);
                let mp = mark_price_map
                    .get(&m.symbol)
                    .copied()
                    .unwrap_or(Decimal::ZERO);
                qty * mp
            }
        })
        .sum();

    let open_orders = OpenOrdersSummary {
        count: orders.len(),
        notional_locked,
    };

    // Lending
    let lending_summary = if args.lending {
        let lend_positions: Vec<_> = borrow_lend
            .iter()
            .filter(|p| p.net_quantity > Decimal::ZERO)
            .collect();
        let total_lent_value: Decimal =
            lend_positions.iter().map(|p| p.net_exposure_notional).sum();
        let daily_income: Decimal = lend_positions.iter().map(|p| p.cumulative_interest).sum();
        Some(LendingSummary {
            total_lent_value,
            daily_income,
        })
    } else {
        None
    };

    let snapshot = PortfolioSnapshot {
        timestamp: chrono::Utc::now().to_rfc3339(),
        account_health,
        pnl,
        collateral: collateral_summary,
        positions: position_entries,
        open_orders,
        lending: lending_summary,
    };

    out::print_portfolio(&snapshot, format);

    Ok(())
}
