use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
use std::io;
use std::time::{Duration, Instant};

use anyhow::Result;
use crossterm::event::{KeyCode, KeyModifiers};
use crossterm::terminal::{
    disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen,
};
use crossterm::ExecutableCommand;
use ratatui::backend::CrosstermBackend;
use ratatui::Terminal;
use rust_decimal::Decimal;

use bpx_api_types::order::{
    CancelOpenOrdersPayload, ExecuteOrderPayload, OrderType, Side, TimeInForce,
};

use crate::client;
use crate::config::{self, ResolvedConfig};
use crate::tui::event::{ActionResult, AppEvent, EventStream, WsMessage};
use crate::tui::keymap::{Action, Keymap};
use crate::tui::trade_cmd::{self, ResolvedAction, TradeCommand};
use crate::tui::{layout, widgets, ws};

const MAX_RECENT_TRADES: usize = 200;
const MAX_KLINE_POINTS: usize = 60;
const MAX_LATENCY_SAMPLES: usize = 120;
const MAX_PING_SAMPLES: usize = 60;
const MAX_PNL_SAMPLES: usize = 120;
const NOTIFICATION_DURATION: Duration = Duration::from_secs(5);
const FUNDING_SNAPSHOT_INTERVAL: Duration = Duration::from_secs(300); // 5 min

pub const KLINE_INTERVALS: &[&str] = &["1m", "5m", "15m", "1h", "4h", "1d"];

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Panel {
    OrderBook,
    Trades,
    Orders,
    Balances,
    Chart,
    Positions,
    Funding,
}

impl Panel {
    pub fn next(self) -> Self {
        match self {
            Panel::OrderBook => Panel::Trades,
            Panel::Trades => Panel::Funding,
            Panel::Funding => Panel::Orders,
            Panel::Orders => Panel::Positions,
            Panel::Positions => Panel::Balances,
            Panel::Balances => Panel::OrderBook,
            Panel::Chart => Panel::Trades,
        }
    }

    pub fn prev(self) -> Self {
        match self {
            Panel::OrderBook => Panel::Balances,
            Panel::Trades => Panel::OrderBook,
            Panel::Funding => Panel::Trades,
            Panel::Orders => Panel::Funding,
            Panel::Positions => Panel::Orders,
            Panel::Balances => Panel::Positions,
            Panel::Chart => Panel::OrderBook,
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InputMode {
    Normal,
    SymbolSearch,
    ConfirmAction(PendingAction),
    OrderEntry,
    CommandBar,
    CommandOutput,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PendingAction {
    CancelOrder {
        symbol: String,
        order_id: String,
    },
    ClosePosition {
        symbol: String,
        side: String,
        quantity: String,
    },
    PlaceOrder {
        symbol: String,
        side: String,
        quantity: String,
        price: Option<String>,
    },
    NukeOrders {
        symbol: String,
        count: usize,
    },
    NukePositions {
        positions: Vec<(String, String, String)>, // (symbol, side, qty)
    },
    NukeAll {
        order_count: usize,
        positions: Vec<(String, String, String)>,
    },
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum NotificationLevel {
    Info,
    Success,
    Error,
}

pub struct Notification {
    pub message: String,
    pub level: NotificationLevel,
    pub created_at: Instant,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OrderFormField {
    Side,
    Price,
    Quantity,
    PostOnly,
    TimeInForce,
    ReduceOnly,
}

impl OrderFormField {
    pub fn next(self) -> Self {
        match self {
            Self::Side => Self::Price,
            Self::Price => Self::Quantity,
            Self::Quantity => Self::PostOnly,
            Self::PostOnly => Self::TimeInForce,
            Self::TimeInForce => Self::ReduceOnly,
            Self::ReduceOnly => Self::Side,
        }
    }

    pub fn prev(self) -> Self {
        match self {
            Self::Side => Self::ReduceOnly,
            Self::Price => Self::Side,
            Self::Quantity => Self::Price,
            Self::PostOnly => Self::Quantity,
            Self::TimeInForce => Self::PostOnly,
            Self::ReduceOnly => Self::TimeInForce,
        }
    }
}

pub struct OrderFormState {
    pub symbol: String,
    pub side: Side,
    pub price_input: String,
    pub quantity_input: String,
    pub post_only: bool,
    pub time_in_force: TimeInForce,
    pub reduce_only: bool,
    pub active_field: OrderFormField,
    pub error_message: Option<String>,
}

/// Maintained order book built from REST snapshot + WebSocket incremental updates.
#[derive(Debug, Default)]
pub struct OrderBook {
    /// Asks sorted ascending by price (lowest ask first)
    pub asks: BTreeMap<Decimal, Decimal>,
    /// Bids sorted descending by price (highest bid first) — we use BTreeMap and reverse when rendering
    pub bids: BTreeMap<Decimal, Decimal>,
}

impl OrderBook {
    /// Apply an incremental depth update. Levels with size 0 are removed.
    pub fn apply_update(&mut self, data: &serde_json::Value) {
        if let Some(asks) = data
            .get("a")
            .or_else(|| data.get("asks"))
            .and_then(|v| v.as_array())
        {
            for level in asks {
                if let Some((price, size)) = parse_level(level) {
                    if size == Decimal::ZERO {
                        self.asks.remove(&price);
                    } else {
                        self.asks.insert(price, size);
                    }
                }
            }
        }
        if let Some(bids) = data
            .get("b")
            .or_else(|| data.get("bids"))
            .and_then(|v| v.as_array())
        {
            for level in bids {
                if let Some((price, size)) = parse_level(level) {
                    if size == Decimal::ZERO {
                        self.bids.remove(&price);
                    } else {
                        self.bids.insert(price, size);
                    }
                }
            }
        }
    }

    /// Seed from REST API snapshot (replaces all levels).
    pub fn seed_from_rest(&mut self, asks: &[(Decimal, Decimal)], bids: &[(Decimal, Decimal)]) {
        self.asks.clear();
        self.bids.clear();
        for (p, s) in asks {
            self.asks.insert(*p, *s);
        }
        for (p, s) in bids {
            self.bids.insert(*p, *s);
        }
    }
}

fn parse_level(value: &serde_json::Value) -> Option<(Decimal, Decimal)> {
    let arr = value.as_array()?;
    let price: Decimal = arr.first()?.as_str()?.parse().ok()?;
    let size: Decimal = arr.get(1)?.as_str()?.parse().ok()?;
    Some((price, size))
}

pub struct CommandOutput {
    pub command: String,
    pub output: String,
    pub success: bool,
    pub scroll_offset: usize,
}

pub struct AppState {
    pub symbol: String,
    pub active_panel: Panel,
    pub show_help: bool,

    // Connection state
    pub ws_connected: bool,
    pub private_ws_connected: bool,
    pub ws_reconnect_count: u32,
    pub ws_last_update: Option<Instant>,

    // Auth
    pub authenticated: bool,

    // Public market data
    pub ticker: Option<serde_json::Value>,
    pub orderbook: OrderBook,
    pub recent_trades: VecDeque<serde_json::Value>,
    pub kline_history: VecDeque<f64>,
    pub mark_price: Option<serde_json::Value>,
    pub all_mark_prices: Vec<serde_json::Value>,
    pub all_tickers: HashMap<String, serde_json::Value>,
    pub open_interest: HashMap<String, serde_json::Value>,
    pub borrow_lend_markets: HashMap<String, serde_json::Value>,

    // Private data (live via WebSocket + REST seed)
    pub open_orders: Vec<serde_json::Value>,
    pub balances: HashMap<String, serde_json::Value>,
    pub positions: Vec<serde_json::Value>,
    #[allow(dead_code)]
    pub recent_fills: VecDeque<serde_json::Value>,
    pub collateral: Option<serde_json::Value>,
    pub borrow_lend_positions: Vec<serde_json::Value>,

    // Markets list for symbol search: (symbol, market_type)
    pub markets: Vec<(String, String)>,

    // Scrolling
    pub scroll_offsets: HashMap<Panel, usize>,

    // Symbol switching
    pub input_mode: InputMode,
    pub symbol_search_input: String,
    pub symbol_search_results: Vec<String>,
    pub symbol_search_selected: usize,
    pub symbol_search_category: usize, // 0 = Spot, 1 = Perps

    // Notification
    pub notification: Option<Notification>,

    // Order form
    pub order_form: Option<OrderFormState>,
    pub pending_order_payload: Option<ExecuteOrderPayload>,

    // Chart interval
    pub kline_interval: usize, // index into KLINE_INTERVALS

    // Command bar
    pub command_input: String,
    pub command_history: Vec<String>,
    pub command_history_index: Option<usize>,
    pub command_output: Option<CommandOutput>,
    pub command_running: Option<String>,

    // Keymap (mutable for runtime bind/unbind)
    pub keymap: Keymap,

    // PnL sparkline history
    pub show_pnl: bool,
    pub pnl_history: Vec<f64>,

    // Collateral breakdown toggle
    pub show_collateral: bool,

    // Funding rate trend cache: symbol -> recent rates
    pub funding_trend_cache: HashMap<String, Vec<f64>>,
    pub last_funding_snapshot: Option<Instant>,

    // Latency tracking
    pub show_latency: bool,
    pub latency_history: VecDeque<crate::tui::event::LatencySample>,
    pub ping_rtt: Option<Duration>,
    pub ping_history: VecDeque<u64>,

    // Basis trade detection: perp symbols that are part of a basis trade
    pub basis_symbols: HashSet<String>,

    // Risk analytics
    pub risk_hhi: Option<f64>,
    pub risk_largest_pct: Option<(String, f64)>,

    // Optimization hints
    pub optimization_hint_count: u32,
}

impl AppState {
    pub fn new(symbol: &str) -> Self {
        Self {
            symbol: symbol.to_string(),
            active_panel: Panel::OrderBook,
            show_help: false,

            ws_connected: false,
            private_ws_connected: false,
            ws_reconnect_count: 0,
            ws_last_update: None,

            authenticated: false,

            ticker: None,
            orderbook: OrderBook::default(),
            recent_trades: VecDeque::with_capacity(MAX_RECENT_TRADES),
            kline_history: VecDeque::with_capacity(MAX_KLINE_POINTS),
            mark_price: None,
            all_mark_prices: Vec::new(),
            all_tickers: HashMap::new(),
            open_interest: HashMap::new(),
            borrow_lend_markets: HashMap::new(),

            open_orders: Vec::new(),
            balances: HashMap::new(),
            positions: Vec::new(),
            recent_fills: VecDeque::with_capacity(MAX_RECENT_TRADES),
            collateral: None,
            borrow_lend_positions: Vec::new(),

            markets: Vec::new(),

            scroll_offsets: HashMap::new(),

            input_mode: InputMode::Normal,
            symbol_search_input: String::new(),
            symbol_search_results: Vec::new(),
            symbol_search_selected: 0,
            symbol_search_category: 0,

            notification: None,
            order_form: None,
            pending_order_payload: None,
            kline_interval: 0,

            command_input: String::new(),
            command_history: Vec::new(),
            command_history_index: None,
            command_output: None,
            command_running: None,
            keymap: Keymap::default_map(),

            show_pnl: false,
            pnl_history: Vec::new(),
            show_collateral: false,
            funding_trend_cache: HashMap::new(),
            last_funding_snapshot: None,

            show_latency: false,
            latency_history: VecDeque::with_capacity(MAX_LATENCY_SAMPLES),
            ping_rtt: None,
            ping_history: VecDeque::with_capacity(MAX_PING_SAMPLES),

            basis_symbols: HashSet::new(),
            risk_hhi: None,
            risk_largest_pct: None,
            optimization_hint_count: 0,
        }
    }

    /// Get scroll offset for the active panel
    pub fn scroll_offset(&self, panel: Panel) -> usize {
        self.scroll_offsets.get(&panel).copied().unwrap_or(0)
    }

    /// Scroll down in the active panel
    pub fn scroll_down(&mut self, panel: Panel, max: usize) {
        let offset = self.scroll_offsets.entry(panel).or_insert(0);
        if *offset < max {
            *offset += 1;
        }
    }

    /// Scroll up in the active panel
    pub fn scroll_up(&mut self, panel: Panel) {
        let offset = self.scroll_offsets.entry(panel).or_insert(0);
        *offset = offset.saturating_sub(1);
    }

    /// Jump to top
    pub fn scroll_to_top(&mut self, panel: Panel) {
        self.scroll_offsets.insert(panel, 0);
    }

    /// Jump to bottom
    pub fn scroll_to_bottom(&mut self, panel: Panel, max: usize) {
        self.scroll_offsets.insert(panel, max);
    }

    /// Clear all market data (used on symbol switch)
    pub fn clear_market_data(&mut self) {
        self.ticker = None;
        self.orderbook = OrderBook::default();
        self.recent_trades.clear();
        self.kline_history.clear();
        self.mark_price = None;
        self.scroll_offsets.clear();
    }

    /// Set a notification with auto-dismiss
    pub fn set_notification(&mut self, message: String, level: NotificationLevel) {
        self.notification = Some(Notification {
            message,
            level,
            created_at: Instant::now(),
        });
    }

    /// Tick notification — auto-dismiss after duration
    pub fn tick_notification(&mut self) {
        if let Some(ref notif) = self.notification {
            if notif.created_at.elapsed() >= NOTIFICATION_DURATION {
                self.notification = None;
            }
        }
    }

    /// Get the current kline interval string
    pub fn current_kline_interval(&self) -> &'static str {
        KLINE_INTERVALS[self.kline_interval]
    }
}

/// Interval for periodic REST refresh of private data (orders, balances, positions).
const PRIVATE_REFRESH_INTERVAL: Duration = Duration::from_secs(30);

pub async fn run(
    symbol: &str,
    config: &ResolvedConfig,
    secret: Option<&str>,
    base_url: Option<&str>,
) -> Result<()> {
    // Install panic hook that restores terminal before printing panic info
    let original_hook = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        let _ = disable_raw_mode();
        let _ = io::stdout().execute(LeaveAlternateScreen);
        original_hook(info);
    }));

    // Setup terminal
    enable_raw_mode()?;
    io::stdout().execute(EnterAlternateScreen)?;
    let backend = CrosstermBackend::new(io::stdout());
    let mut terminal = Terminal::new(backend)?;
    terminal.clear()?;

    let mut state = AppState::new(symbol);

    // Load user keybinding overrides from config
    let user_keybindings = config::load_keybindings();
    if !user_keybindings.is_empty() {
        state.keymap = Keymap::with_overrides(&user_keybindings);
    }

    // Show splash while seeding data
    terminal.draw(|f| {
        widgets::splash::render(f, f.area());
    })?;

    // Auth detection
    let auth_client = client::authenticated_client(config, secret, base_url).ok();
    state.authenticated = auth_client.is_some();

    // Create event stream + WS channels early so WS connects in parallel with REST
    let (mut events, event_tx) = EventStream::new(Duration::from_millis(250));
    let (symbol_tx, _symbol_rx) = tokio::sync::watch::channel(symbol.to_string());

    // Channel for kline interval — ws needs to know current interval
    let initial_params = ws::SubscriptionParams {
        symbol: symbol.to_string(),
        kline_interval: KLINE_INTERVALS[0].to_string(),
    };
    let (params_tx, params_rx) = tokio::sync::watch::channel(initial_params);

    // Spawn public WebSocket immediately — don't wait for REST seeds
    let ws_tx = event_tx.clone();
    let ws_url = config.ws_url.clone();
    tokio::spawn(async move {
        ws::connect_public(ws_url, ws_tx, params_rx).await;
    });

    // Spawn private WebSocket if authenticated
    if let Some(ref _ac) = auth_client {
        if let Some(ref api_secret) = config.api_secret {
            let priv_tx = event_tx.clone();
            let priv_ws_url = config.ws_url.clone();
            let priv_secret = api_secret.clone();
            tokio::spawn(async move {
                ws::connect_private(priv_ws_url, priv_secret, priv_tx).await;
            });
        }
    }

    // REST seed — run all public calls concurrently
    let public_client = client::public_client(config, base_url).ok();
    if let Some(ref c) = public_client {
        let start_time = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_secs() as i64
            - 3600;

        let (depth_res, markets_res, klines_res, mark_res, tickers_res) = tokio::join!(
            c.get_order_book_depth(symbol, None),
            c.get_markets(),
            c.get_k_lines(symbol, "1m", start_time, None),
            c.get_all_mark_prices(),
            c.get_tickers(),
        );

        if let Ok(depth) = depth_res {
            state.orderbook.seed_from_rest(&depth.asks, &depth.bids);
        }
        if let Ok(markets) = markets_res {
            state.markets = markets
                .iter()
                .map(|m| (m.symbol.clone(), m.market_type.clone()))
                .collect();
        }
        if let Ok(klines) = klines_res {
            for k in klines.iter().rev().take(MAX_KLINE_POINTS).rev() {
                if let Some(close) = k.close {
                    if let Ok(close_f64) = close.to_string().parse::<f64>() {
                        state.kline_history.push_back(close_f64);
                    }
                }
            }
        }
        if let Ok(mark_prices) = mark_res {
            if let Some(mp) = mark_prices.iter().find(|m| m.symbol == symbol) {
                state.mark_price = serde_json::to_value(mp).ok();
            }
            state.all_mark_prices = mark_prices
                .iter()
                .filter_map(|mp| serde_json::to_value(mp).ok())
                .collect();
        }
        if let Ok(tickers) = tickers_res {
            for t in tickers {
                if let Ok(v) = serde_json::to_value(&t) {
                    state.all_tickers.insert(t.symbol.clone(), v);
                }
            }
        }

        // Fetch open interest (not in SDK yet — raw REST call)
        fetch_open_interest(&mut state, c, base_url, config).await;
        // Fetch borrow/lend market rates
        fetch_borrow_lend_markets(&mut state, c, base_url, config).await;
    }

    // REST seed — private data (if authenticated)
    if let Some(ref c) = auth_client {
        seed_private_data(&mut state, c).await;
    }

    // Seed funding trend cache from history file
    for mp in &state.all_mark_prices {
        if let Some(sym) = mp.get("symbol").and_then(|v| v.as_str()) {
            let recent = crate::history::load_recent(sym, 24);
            if !recent.is_empty() {
                let rates: Vec<f64> = recent.iter().map(|s| s.funding_rate).collect();
                state.funding_trend_cache.insert(sym.to_string(), rates);
            }
        }
    }

    // Track last private data refresh time
    let mut last_private_refresh = Instant::now();

    // Main loop
    'main: loop {
        // Draw
        terminal.draw(|f| {
            let ly = layout::compute(f.area());
            widgets::ticker::render(f, ly.ticker_bar, &state);
            widgets::orderbook::render(f, ly.orderbook, &state);
            widgets::trades::render(f, ly.trades, &state);

            // Chart and funding (always visible on wide terminals)
            if let Some(chart_area) = ly.chart {
                widgets::chart::render(f, chart_area, &state);
            }
            if let Some(funding_area) = ly.funding {
                widgets::funding::render(f, funding_area, &state);
            }
            if let Some(details_area) = ly.symbol_details {
                if state.show_pnl {
                    widgets::pnl_chart::render(f, details_area, &state);
                } else {
                    widgets::account_info::render(f, details_area, &state);
                }
            }

            widgets::orders::render(f, ly.orders, &state);
            widgets::positions::render(f, ly.positions, &state);
            widgets::balances::render(f, ly.balances, &state);

            // Status bar or command bar
            if state.input_mode == InputMode::CommandBar || state.command_running.is_some() {
                widgets::command_bar::render(f, ly.status_bar, &state);
            } else {
                widgets::status_bar::render(f, ly.status_bar, &state);
            }

            // Overlays
            if state.show_help {
                widgets::help::render(f, f.area());
            }
            if state.input_mode == InputMode::SymbolSearch {
                widgets::symbol_search::render(f, f.area(), &state);
            }
            if let InputMode::ConfirmAction(ref action) = state.input_mode {
                widgets::confirm_dialog::render(f, f.area(), action);
            }
            if state.input_mode == InputMode::OrderEntry {
                if let Some(ref form) = state.order_form {
                    widgets::order_form::render(f, f.area(), form, &state);
                }
            }
            if state.input_mode == InputMode::CommandOutput {
                if let Some(ref output) = state.command_output {
                    widgets::command_output::render(f, f.area(), output);
                }
            }
        })?;

        // Handle events
        if let Some(event) = events.next().await {
            match event {
                AppEvent::Key(key) => {
                    // Ctrl-C always quits
                    if key.modifiers.contains(KeyModifiers::CONTROL)
                        && key.code == KeyCode::Char('c')
                    {
                        break;
                    }

                    // Confirm dialog mode
                    if let InputMode::ConfirmAction(ref action) = state.input_mode {
                        let action = action.clone();
                        match key.code {
                            KeyCode::Char('y') | KeyCode::Enter => {
                                // Execute the action
                                match &action {
                                    PendingAction::CancelOrder { symbol, order_id } => {
                                        if let Some(ref c) = auth_client {
                                            let c = c.clone();
                                            let tx = event_tx.clone();
                                            let sym = symbol.clone();
                                            let oid = order_id.clone();
                                            tokio::spawn(async move {
                                                let result =
                                                    c.cancel_order(&sym, Some(&oid), None).await;
                                                let action_result = match result {
                                                    Ok(_) => ActionResult::OrderCancelled {
                                                        symbol: sym,
                                                        order_id: oid,
                                                    },
                                                    Err(e) => ActionResult::OrderFailed {
                                                        message: e.to_string(),
                                                    },
                                                };
                                                let _ =
                                                    tx.send(AppEvent::ActionResult(action_result));
                                            });
                                        }
                                    }
                                    PendingAction::ClosePosition {
                                        symbol,
                                        side,
                                        quantity,
                                    } => {
                                        if let Some(ref c) = auth_client {
                                            let c = c.clone();
                                            let tx = event_tx.clone();
                                            let sym = symbol.clone();
                                            let close_side =
                                                if side == "LONG" { Side::Ask } else { Side::Bid };
                                            let qty: Decimal =
                                                quantity.parse().unwrap_or(Decimal::ZERO);
                                            let payload = ExecuteOrderPayload {
                                                symbol: sym.clone(),
                                                side: close_side,
                                                order_type: OrderType::Market,
                                                time_in_force: Some(TimeInForce::IOC),
                                                quantity: Some(qty),
                                                reduce_only: Some(true),
                                                ..Default::default()
                                            };
                                            tokio::spawn(async move {
                                                let result = c.execute_order(payload).await;
                                                let action_result = match result {
                                                    Ok(_) => {
                                                        ActionResult::PositionClosed { symbol: sym }
                                                    }
                                                    Err(e) => ActionResult::PositionCloseFailed {
                                                        message: e.to_string(),
                                                    },
                                                };
                                                let _ =
                                                    tx.send(AppEvent::ActionResult(action_result));
                                            });
                                        }
                                    }
                                    PendingAction::PlaceOrder { .. } => {
                                        if let Some(ref c) = auth_client {
                                            if let Some(payload) =
                                                state.pending_order_payload.take()
                                            {
                                                let c = c.clone();
                                                let tx = event_tx.clone();
                                                let sym = payload.symbol.clone();
                                                let side_str = if payload.side == Side::Bid {
                                                    "BUY"
                                                } else {
                                                    "SELL"
                                                }
                                                .to_string();
                                                let qty_str = payload
                                                    .quantity
                                                    .map(|q| q.to_string())
                                                    .unwrap_or_default();
                                                let price_str =
                                                    payload.price.map(|p| p.to_string());
                                                tokio::spawn(async move {
                                                    let result = c.execute_order(payload).await;
                                                    let action_result = match result {
                                                        Ok(_) => ActionResult::OrderPlaced {
                                                            symbol: sym,
                                                            side: side_str,
                                                            qty: qty_str,
                                                            price: price_str,
                                                        },
                                                        Err(e) => ActionResult::OrderFailed {
                                                            message: e.to_string(),
                                                        },
                                                    };
                                                    let _ = tx.send(AppEvent::ActionResult(
                                                        action_result,
                                                    ));
                                                });
                                            }
                                        }
                                    }
                                    PendingAction::NukeOrders { symbol, count } => {
                                        if let Some(ref c) = auth_client {
                                            let c = c.clone();
                                            let tx = event_tx.clone();
                                            let sym = symbol.clone();
                                            let cnt = *count;
                                            tokio::spawn(async move {
                                                let payload = CancelOpenOrdersPayload {
                                                    symbol: sym.clone(),
                                                };
                                                let result = c.cancel_open_orders(payload).await;
                                                let action_result = match result {
                                                    Ok(_) => ActionResult::OrdersCancelled {
                                                        symbol: sym,
                                                        count: cnt,
                                                    },
                                                    Err(e) => ActionResult::OrdersCancelFailed {
                                                        message: e.to_string(),
                                                    },
                                                };
                                                let _ =
                                                    tx.send(AppEvent::ActionResult(action_result));
                                            });
                                        }
                                    }
                                    PendingAction::NukePositions { positions } => {
                                        if let Some(ref c) = auth_client {
                                            let c = c.clone();
                                            let tx = event_tx.clone();
                                            let positions = positions.clone();
                                            tokio::spawn(async move {
                                                let mut closed = 0usize;
                                                let mut last_err = None;
                                                for (sym, side, qty_str) in &positions {
                                                    let close_side = if side == "LONG" {
                                                        Side::Ask
                                                    } else {
                                                        Side::Bid
                                                    };
                                                    let qty: Decimal =
                                                        qty_str.parse().unwrap_or(Decimal::ZERO);
                                                    let payload = ExecuteOrderPayload {
                                                        symbol: sym.clone(),
                                                        side: close_side,
                                                        order_type: OrderType::Market,
                                                        time_in_force: Some(TimeInForce::IOC),
                                                        quantity: Some(qty),
                                                        reduce_only: Some(true),
                                                        ..Default::default()
                                                    };
                                                    match c.execute_order(payload).await {
                                                        Ok(_) => closed += 1,
                                                        Err(e) => last_err = Some(e.to_string()),
                                                    }
                                                }
                                                let action_result = if let Some(err) = last_err {
                                                    ActionResult::NukeFailed {
                                                        message: format!(
                                                            "Closed {closed}/{}, last error: {err}",
                                                            positions.len()
                                                        ),
                                                    }
                                                } else {
                                                    ActionResult::NukeCompleted {
                                                        orders_cancelled: 0,
                                                        positions_closed: closed,
                                                    }
                                                };
                                                let _ =
                                                    tx.send(AppEvent::ActionResult(action_result));
                                            });
                                        }
                                    }
                                    PendingAction::NukeAll {
                                        order_count: _,
                                        positions,
                                    } => {
                                        if let Some(ref c) = auth_client {
                                            let c = c.clone();
                                            let tx = event_tx.clone();
                                            // Build per-symbol order counts
                                            let mut sym_counts: HashMap<String, usize> =
                                                HashMap::new();
                                            for o in &state.open_orders {
                                                if let Some(sym) = o
                                                    .get("symbol")
                                                    .or_else(|| o.get("s"))
                                                    .and_then(|v| v.as_str())
                                                {
                                                    *sym_counts
                                                        .entry(sym.to_string())
                                                        .or_default() += 1;
                                                }
                                            }
                                            let positions = positions.clone();
                                            tokio::spawn(async move {
                                                // Cancel all orders per symbol
                                                let mut cancelled = 0usize;
                                                for (sym, count) in &sym_counts {
                                                    let payload = CancelOpenOrdersPayload {
                                                        symbol: sym.clone(),
                                                    };
                                                    if c.cancel_open_orders(payload).await.is_ok() {
                                                        cancelled += count;
                                                    }
                                                }

                                                // Close all positions
                                                let mut closed = 0usize;
                                                let mut last_err = None;
                                                for (sym, side, qty_str) in &positions {
                                                    let close_side = if side == "LONG" {
                                                        Side::Ask
                                                    } else {
                                                        Side::Bid
                                                    };
                                                    let qty: Decimal =
                                                        qty_str.parse().unwrap_or(Decimal::ZERO);
                                                    let payload = ExecuteOrderPayload {
                                                        symbol: sym.clone(),
                                                        side: close_side,
                                                        order_type: OrderType::Market,
                                                        time_in_force: Some(TimeInForce::IOC),
                                                        quantity: Some(qty),
                                                        reduce_only: Some(true),
                                                        ..Default::default()
                                                    };
                                                    match c.execute_order(payload).await {
                                                        Ok(_) => closed += 1,
                                                        Err(e) => last_err = Some(e.to_string()),
                                                    }
                                                }

                                                let action_result = if let Some(err) = last_err {
                                                    ActionResult::NukeFailed {
                                                        message: format!(
                                                            "Cancelled {cancelled} orders, closed {closed}/{} positions, last error: {err}",
                                                            positions.len()
                                                        ),
                                                    }
                                                } else {
                                                    ActionResult::NukeCompleted {
                                                        orders_cancelled: cancelled,
                                                        positions_closed: closed,
                                                    }
                                                };
                                                let _ =
                                                    tx.send(AppEvent::ActionResult(action_result));
                                            });
                                        }
                                    }
                                }
                                state.input_mode = InputMode::Normal;
                            }
                            KeyCode::Char('n') | KeyCode::Esc => {
                                state.pending_order_payload = None;
                                state.input_mode = InputMode::Normal;
                            }
                            _ => {}
                        }
                        continue;
                    }

                    // Order entry mode
                    if state.input_mode == InputMode::OrderEntry {
                        if let Some(ref mut form) = state.order_form {
                            match key.code {
                                KeyCode::Esc => {
                                    state.order_form = None;
                                    state.input_mode = InputMode::Normal;
                                }
                                KeyCode::Tab => {
                                    form.active_field = form.active_field.next();
                                }
                                KeyCode::BackTab => {
                                    form.active_field = form.active_field.prev();
                                }
                                KeyCode::Enter => {
                                    // Validate and submit
                                    let qty_result: Result<Decimal, _> =
                                        form.quantity_input.parse();
                                    if form.quantity_input.is_empty() || qty_result.is_err() {
                                        form.error_message = Some("Invalid quantity".to_string());
                                        continue;
                                    }
                                    let qty = qty_result.unwrap();

                                    let price_dec = if form.price_input.is_empty() {
                                        None
                                    } else {
                                        match form.price_input.parse::<Decimal>() {
                                            Ok(p) => Some(p),
                                            Err(_) => {
                                                form.error_message =
                                                    Some("Invalid price".to_string());
                                                continue;
                                            }
                                        }
                                    };

                                    let order_type = if price_dec.is_some() {
                                        OrderType::Limit
                                    } else {
                                        OrderType::Market
                                    };

                                    // Extract values before dropping the borrow
                                    let form_symbol = form.symbol.clone();
                                    let form_side = form.side;
                                    let form_tif = form.time_in_force;
                                    let form_post_only = form.post_only;
                                    let form_reduce_only = form.reduce_only;

                                    let payload = ExecuteOrderPayload {
                                        symbol: form_symbol.clone(),
                                        side: form_side,
                                        order_type,
                                        time_in_force: Some(form_tif),
                                        quantity: Some(qty),
                                        price: price_dec,
                                        post_only: Some(form_post_only),
                                        reduce_only: Some(form_reduce_only),
                                        ..Default::default()
                                    };

                                    let side_str = if form_side == Side::Bid {
                                        "BUY"
                                    } else {
                                        "SELL"
                                    }
                                    .to_string();
                                    let qty_str = qty.to_string();
                                    let price_str = price_dec.map(|p| p.to_string());

                                    state.pending_order_payload = Some(payload);
                                    state.order_form = None;
                                    state.input_mode =
                                        InputMode::ConfirmAction(PendingAction::PlaceOrder {
                                            symbol: form_symbol,
                                            side: side_str,
                                            quantity: qty_str,
                                            price: price_str,
                                        });
                                }
                                KeyCode::Left | KeyCode::Right => match form.active_field {
                                    OrderFormField::Side => {
                                        form.side = if form.side == Side::Bid {
                                            Side::Ask
                                        } else {
                                            Side::Bid
                                        };
                                    }
                                    OrderFormField::TimeInForce => {
                                        form.time_in_force = match form.time_in_force {
                                            TimeInForce::GTC => TimeInForce::IOC,
                                            TimeInForce::IOC => TimeInForce::FOK,
                                            TimeInForce::FOK => TimeInForce::GTC,
                                        };
                                    }
                                    _ => {}
                                },
                                KeyCode::Char(' ') => match form.active_field {
                                    OrderFormField::Side => {
                                        form.side = if form.side == Side::Bid {
                                            Side::Ask
                                        } else {
                                            Side::Bid
                                        };
                                    }
                                    OrderFormField::PostOnly => {
                                        form.post_only = !form.post_only;
                                    }
                                    OrderFormField::ReduceOnly => {
                                        form.reduce_only = !form.reduce_only;
                                    }
                                    OrderFormField::TimeInForce => {
                                        form.time_in_force = match form.time_in_force {
                                            TimeInForce::GTC => TimeInForce::IOC,
                                            TimeInForce::IOC => TimeInForce::FOK,
                                            TimeInForce::FOK => TimeInForce::GTC,
                                        };
                                    }
                                    _ => {}
                                },
                                KeyCode::Char(c)
                                    if (c.is_ascii_digit() || c == '.')
                                        && matches!(
                                            form.active_field,
                                            OrderFormField::Price | OrderFormField::Quantity
                                        ) =>
                                {
                                    // Quick size presets: 1=25%, 2=50%, 3=75%, 4=100% when Qty is active
                                    if form.active_field == OrderFormField::Quantity
                                        && form.quantity_input.is_empty()
                                        && matches!(c, '1' | '2' | '3' | '4')
                                    {
                                        let pct = match c {
                                            '1' => 0.25,
                                            '2' => 0.50,
                                            '3' => 0.75,
                                            '4' => 1.00,
                                            _ => 0.0,
                                        };
                                        let available_usdc: f64 = state
                                            .balances
                                            .get("USDC")
                                            .and_then(|v| {
                                                v.get("available")
                                                    .and_then(|x| x.as_str())
                                                    .and_then(|s| s.parse().ok())
                                            })
                                            .unwrap_or(0.0);
                                        let ref_price: f64 = if !form.price_input.is_empty() {
                                            form.price_input.parse().unwrap_or(0.0)
                                        } else {
                                            state
                                                .ticker
                                                .as_ref()
                                                .and_then(|t| {
                                                    t.get("lastPrice")
                                                        .or_else(|| t.get("c"))
                                                        .and_then(|v| v.as_str())
                                                        .and_then(|s| s.parse().ok())
                                                })
                                                .unwrap_or(0.0)
                                        };
                                        if ref_price > 0.0 && available_usdc > 0.0 {
                                            let notional = available_usdc * pct;
                                            let qty = notional / ref_price;
                                            form.quantity_input = format!("{qty:.4}");
                                            form.error_message = None;
                                            continue;
                                        }
                                    }
                                    match form.active_field {
                                        OrderFormField::Price => form.price_input.push(c),
                                        OrderFormField::Quantity => form.quantity_input.push(c),
                                        _ => {}
                                    }
                                    form.error_message = None;
                                }
                                KeyCode::Backspace => match form.active_field {
                                    OrderFormField::Price => {
                                        form.price_input.pop();
                                    }
                                    OrderFormField::Quantity => {
                                        form.quantity_input.pop();
                                    }
                                    _ => {}
                                },
                                _ => {}
                            }
                        }
                        continue;
                    }

                    // Symbol search mode
                    if state.input_mode == InputMode::SymbolSearch {
                        match key.code {
                            KeyCode::Esc => {
                                state.input_mode = InputMode::Normal;
                                state.symbol_search_input.clear();
                                state.symbol_search_results.clear();
                                state.symbol_search_selected = 0;
                            }
                            KeyCode::Tab | KeyCode::Left | KeyCode::Right => {
                                // Toggle Spot / Perps
                                state.symbol_search_category = 1 - state.symbol_search_category;
                                state.symbol_search_results = filter_markets(
                                    &state.markets,
                                    &state.symbol_search_input,
                                    state.symbol_search_category,
                                );
                                state.symbol_search_selected = 0;
                            }
                            KeyCode::Up => {
                                state.symbol_search_selected =
                                    state.symbol_search_selected.saturating_sub(1);
                            }
                            KeyCode::Down => {
                                let max = state.symbol_search_results.len().saturating_sub(1);
                                if state.symbol_search_selected < max {
                                    state.symbol_search_selected += 1;
                                }
                            }
                            KeyCode::Enter => {
                                let selected = state
                                    .symbol_search_results
                                    .get(state.symbol_search_selected)
                                    .cloned();
                                if let Some(new_symbol) = selected {
                                    state.symbol = new_symbol.clone();
                                    state.clear_market_data();
                                    state.kline_interval = 0;
                                    let _ = symbol_tx.send(new_symbol.clone());
                                    let _ = params_tx.send(ws::SubscriptionParams {
                                        symbol: new_symbol.clone(),
                                        kline_interval: KLINE_INTERVALS[0].to_string(),
                                    });

                                    // Re-seed from REST
                                    if let Some(ref c) = public_client {
                                        if let Ok(depth) =
                                            c.get_order_book_depth(&new_symbol, None).await
                                        {
                                            state
                                                .orderbook
                                                .seed_from_rest(&depth.asks, &depth.bids);
                                        }
                                        let start_time = std::time::SystemTime::now()
                                            .duration_since(std::time::UNIX_EPOCH)
                                            .unwrap()
                                            .as_secs()
                                            as i64
                                            - 3600;
                                        if let Ok(klines) =
                                            c.get_k_lines(&new_symbol, "1m", start_time, None).await
                                        {
                                            for k in
                                                klines.iter().rev().take(MAX_KLINE_POINTS).rev()
                                            {
                                                if let Some(close) = k.close {
                                                    if let Ok(close_f64) =
                                                        close.to_string().parse::<f64>()
                                                    {
                                                        state.kline_history.push_back(close_f64);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                                state.input_mode = InputMode::Normal;
                                state.symbol_search_input.clear();
                                state.symbol_search_results.clear();
                                state.symbol_search_selected = 0;
                            }
                            KeyCode::Backspace => {
                                state.symbol_search_input.pop();
                                state.symbol_search_results = filter_markets(
                                    &state.markets,
                                    &state.symbol_search_input,
                                    state.symbol_search_category,
                                );
                                state.symbol_search_selected = 0;
                            }
                            KeyCode::Char(c) => {
                                state.symbol_search_input.push(c);
                                state.symbol_search_results = filter_markets(
                                    &state.markets,
                                    &state.symbol_search_input,
                                    state.symbol_search_category,
                                );
                                state.symbol_search_selected = 0;
                            }
                            _ => {}
                        }
                        continue;
                    }

                    // Command bar mode
                    if state.input_mode == InputMode::CommandBar {
                        match key.code {
                            KeyCode::Esc => {
                                state.input_mode = InputMode::Normal;
                                state.command_input.clear();
                                state.command_history_index = None;
                            }
                            KeyCode::Enter => {
                                let input = state.command_input.trim().to_string();
                                if input.is_empty() {
                                    state.input_mode = InputMode::Normal;
                                    state.command_input.clear();
                                    state.command_history_index = None;
                                    continue;
                                }

                                state.command_history.push(input.clone());
                                state.command_history_index = None;
                                state.command_input.clear();

                                // Try TUI-internal commands first
                                match parse_tui_command(&input) {
                                    Some(TuiCommand::Quit) => break 'main,
                                    Some(TuiCommand::Help) => {
                                        state.show_help = !state.show_help;
                                        state.input_mode = InputMode::Normal;
                                    }
                                    Some(TuiCommand::Search) => {
                                        state.input_mode = InputMode::SymbolSearch;
                                        state.symbol_search_input.clear();
                                        state.symbol_search_category = 0;
                                        state.symbol_search_results = filter_markets(
                                            &state.markets,
                                            "",
                                            state.symbol_search_category,
                                        );
                                        state.symbol_search_selected = 0;
                                    }
                                    Some(TuiCommand::Symbol(sym)) => {
                                        let sym_upper = sym.to_uppercase();
                                        if state.markets.iter().any(|(s, _)| s == &sym_upper) {
                                            state.symbol = sym_upper.clone();
                                            state.clear_market_data();
                                            state.kline_interval = 0;
                                            let _ = symbol_tx.send(sym_upper.clone());
                                            let _ = params_tx.send(ws::SubscriptionParams {
                                                symbol: sym_upper.clone(),
                                                kline_interval: KLINE_INTERVALS[0].to_string(),
                                            });
                                            if let Some(ref c) = public_client {
                                                if let Ok(depth) =
                                                    c.get_order_book_depth(&sym_upper, None).await
                                                {
                                                    state
                                                        .orderbook
                                                        .seed_from_rest(&depth.asks, &depth.bids);
                                                }
                                                let start_time = std::time::SystemTime::now()
                                                    .duration_since(std::time::UNIX_EPOCH)
                                                    .unwrap()
                                                    .as_secs()
                                                    as i64
                                                    - 3600;
                                                if let Ok(klines) = c
                                                    .get_k_lines(&sym_upper, "1m", start_time, None)
                                                    .await
                                                {
                                                    for k in klines
                                                        .iter()
                                                        .rev()
                                                        .take(MAX_KLINE_POINTS)
                                                        .rev()
                                                    {
                                                        if let Some(close) = k.close {
                                                            if let Ok(f) =
                                                                close.to_string().parse::<f64>()
                                                            {
                                                                state.kline_history.push_back(f);
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                            state.set_notification(
                                                format!("Switched to {sym_upper}"),
                                                NotificationLevel::Info,
                                            );
                                        } else {
                                            state.set_notification(
                                                format!("Unknown symbol: {sym}"),
                                                NotificationLevel::Error,
                                            );
                                        }
                                        state.input_mode = InputMode::Normal;
                                    }
                                    Some(TuiCommand::Order) => {
                                        if state.authenticated {
                                            let best_ask = state
                                                .orderbook
                                                .asks
                                                .keys()
                                                .next()
                                                .map(|p| p.to_string())
                                                .unwrap_or_default();
                                            state.order_form = Some(OrderFormState {
                                                symbol: state.symbol.clone(),
                                                side: Side::Bid,
                                                price_input: best_ask,
                                                quantity_input: String::new(),
                                                post_only: false,
                                                time_in_force: TimeInForce::GTC,
                                                reduce_only: false,
                                                active_field: OrderFormField::Quantity,
                                                error_message: None,
                                            });
                                            state.input_mode = InputMode::OrderEntry;
                                        } else {
                                            state.set_notification(
                                                "Not authenticated".to_string(),
                                                NotificationLevel::Error,
                                            );
                                            state.input_mode = InputMode::Normal;
                                        }
                                    }
                                    Some(TuiCommand::Cancel) => {
                                        state.input_mode = InputMode::Normal;
                                        if state.active_panel == Panel::Orders
                                            && state.authenticated
                                            && !state.open_orders.is_empty()
                                        {
                                            let scroll = state.scroll_offset(Panel::Orders);
                                            if let Some(order) = state.open_orders.get(scroll) {
                                                let symbol = order
                                                    .get("symbol")
                                                    .or_else(|| order.get("s"))
                                                    .and_then(|v| v.as_str())
                                                    .unwrap_or("")
                                                    .to_string();
                                                let order_id = order
                                                    .get("id")
                                                    .and_then(|v| v.as_str())
                                                    .unwrap_or("")
                                                    .to_string();
                                                if !order_id.is_empty() {
                                                    state.input_mode = InputMode::ConfirmAction(
                                                        PendingAction::CancelOrder {
                                                            symbol,
                                                            order_id,
                                                        },
                                                    );
                                                }
                                            }
                                        }
                                    }
                                    Some(TuiCommand::Close) => {
                                        state.input_mode = InputMode::Normal;
                                        if state.active_panel == Panel::Positions
                                            && state.authenticated
                                            && !state.positions.is_empty()
                                        {
                                            let scroll = state.scroll_offset(Panel::Positions);
                                            if let Some(pos) = state.positions.get(scroll) {
                                                let symbol = pos
                                                    .get("symbol")
                                                    .and_then(|v| v.as_str())
                                                    .unwrap_or("")
                                                    .to_string();
                                                let qty_str = pos
                                                    .get("netQuantity")
                                                    .or_else(|| pos.get("net_quantity"))
                                                    .and_then(|v| v.as_str())
                                                    .unwrap_or("0");
                                                let qty: f64 = qty_str.parse().unwrap_or(0.0);
                                                if qty != 0.0 {
                                                    let side = if qty >= 0.0 {
                                                        "LONG".to_string()
                                                    } else {
                                                        "SHORT".to_string()
                                                    };
                                                    let abs_qty = format!("{:.8}", qty.abs());
                                                    state.input_mode = InputMode::ConfirmAction(
                                                        PendingAction::ClosePosition {
                                                            symbol,
                                                            side,
                                                            quantity: abs_qty,
                                                        },
                                                    );
                                                }
                                            }
                                        }
                                    }
                                    Some(TuiCommand::Interval(interval)) => {
                                        if let Some(idx) =
                                            KLINE_INTERVALS.iter().position(|&i| i == interval)
                                        {
                                            state.kline_interval = idx;
                                            state.kline_history.clear();
                                            let _ = params_tx.send(ws::SubscriptionParams {
                                                symbol: state.symbol.clone(),
                                                kline_interval: interval.clone(),
                                            });
                                            if let Some(ref c) = public_client {
                                                let window = kline_window_secs(&interval);
                                                let start = std::time::SystemTime::now()
                                                    .duration_since(std::time::UNIX_EPOCH)
                                                    .unwrap()
                                                    .as_secs()
                                                    as i64
                                                    - window;
                                                if let Ok(klines) = c
                                                    .get_k_lines(
                                                        &state.symbol,
                                                        &interval,
                                                        start,
                                                        None,
                                                    )
                                                    .await
                                                {
                                                    for k in klines
                                                        .iter()
                                                        .rev()
                                                        .take(MAX_KLINE_POINTS)
                                                        .rev()
                                                    {
                                                        if let Some(close) = k.close {
                                                            if let Ok(f) =
                                                                close.to_string().parse::<f64>()
                                                            {
                                                                state.kline_history.push_back(f);
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                            state.set_notification(
                                                format!("Chart interval: {interval}"),
                                                NotificationLevel::Info,
                                            );
                                        } else {
                                            state.set_notification(
                                                format!(
                                                    "Invalid interval. Use: {}",
                                                    KLINE_INTERVALS.join(", ")
                                                ),
                                                NotificationLevel::Error,
                                            );
                                        }
                                        state.input_mode = InputMode::Normal;
                                    }
                                    Some(TuiCommand::Panel(name)) => {
                                        let panel = match name.to_lowercase().as_str() {
                                            "1" | "orderbook" | "ob" => Some(Panel::OrderBook),
                                            "2" | "trades" => Some(Panel::Trades),
                                            "3" | "funding" | "fund" => Some(Panel::Funding),
                                            "4" | "orders" => Some(Panel::Orders),
                                            "5" | "positions" | "pos" => Some(Panel::Positions),
                                            "6" | "balances" | "bal" => Some(Panel::Balances),
                                            _ => None,
                                        };
                                        if let Some(p) = panel {
                                            state.active_panel = p;
                                        } else {
                                            state.set_notification(
                                                format!("Unknown panel: {name}"),
                                                NotificationLevel::Error,
                                            );
                                        }
                                        state.input_mode = InputMode::Normal;
                                    }
                                    Some(TuiCommand::Refresh) => {
                                        if let Some(ref c) = auth_client {
                                            seed_private_data(&mut state, c).await;
                                            last_private_refresh = Instant::now();
                                            state.set_notification(
                                                "Refreshed".to_string(),
                                                NotificationLevel::Info,
                                            );
                                        }
                                        state.input_mode = InputMode::Normal;
                                    }
                                    Some(TuiCommand::Bind { key, action }) => {
                                        match state.keymap.bind(&key, &action) {
                                            Ok(()) => {
                                                // Persist to config
                                                if let Err(e) = config::save_keybindings(
                                                    state.keymap.user_overrides(),
                                                ) {
                                                    state.set_notification(
                                                        format!("Bound {key} but save failed: {e}"),
                                                        NotificationLevel::Error,
                                                    );
                                                } else {
                                                    state.set_notification(
                                                        format!("Bound {key} \u{2192} {action}"),
                                                        NotificationLevel::Success,
                                                    );
                                                }
                                            }
                                            Err(e) => {
                                                state.set_notification(e, NotificationLevel::Error);
                                            }
                                        }
                                        state.input_mode = InputMode::Normal;
                                    }
                                    Some(TuiCommand::Unbind(key)) => {
                                        match state.keymap.unbind(&key) {
                                            Ok(()) => {
                                                if let Err(e) = config::save_keybindings(
                                                    state.keymap.user_overrides(),
                                                ) {
                                                    state.set_notification(
                                                        format!(
                                                            "Unbound {key} but save failed: {e}"
                                                        ),
                                                        NotificationLevel::Error,
                                                    );
                                                } else {
                                                    state.set_notification(
                                                        format!("Unbound {key}"),
                                                        NotificationLevel::Info,
                                                    );
                                                }
                                            }
                                            Err(e) => {
                                                state.set_notification(e, NotificationLevel::Error);
                                            }
                                        }
                                        state.input_mode = InputMode::Normal;
                                    }
                                    Some(TuiCommand::UnbindAll) => {
                                        state.keymap.unbind_all();
                                        if let Err(e) =
                                            config::save_keybindings(state.keymap.user_overrides())
                                        {
                                            state.set_notification(
                                                format!("Reset bindings but save failed: {e}"),
                                                NotificationLevel::Error,
                                            );
                                        } else {
                                            state.set_notification(
                                                "All custom bindings removed".to_string(),
                                                NotificationLevel::Info,
                                            );
                                        }
                                        state.input_mode = InputMode::Normal;
                                    }
                                    Some(TuiCommand::BindList) => {
                                        let output = state.keymap.list_bindings();
                                        state.command_output = Some(CommandOutput {
                                            command: "bind list".to_string(),
                                            output,
                                            success: true,
                                            scroll_offset: 0,
                                        });
                                        state.input_mode = InputMode::CommandOutput;
                                    }
                                    Some(TuiCommand::Scenario { args }) => {
                                        let output = compute_scenario(&args, &state);
                                        state.command_output = Some(CommandOutput {
                                            command: format!("scenario {args}"),
                                            output,
                                            success: true,
                                            scroll_offset: 0,
                                        });
                                        state.input_mode = InputMode::CommandOutput;
                                    }
                                    Some(TuiCommand::Risk) => {
                                        let output = compute_risk_report(&state);
                                        state.command_output = Some(CommandOutput {
                                            command: "risk".to_string(),
                                            output,
                                            success: true,
                                            scroll_offset: 0,
                                        });
                                        state.input_mode = InputMode::CommandOutput;
                                    }
                                    Some(TuiCommand::StrategyList) => {
                                        let output = match config::load_strategies() {
                                            Ok(strategies) if strategies.is_empty() => {
                                                "No saved strategies.\nUse `bpx strategy save <name>` to create one.".to_string()
                                            }
                                            Ok(strategies) => {
                                                let mut lines = vec![format!("{:<12} {:<16} {:>5} {:>8} {:>8} {:>8}", "NAME", "SYMBOL", "SIDE", "SIZE", "SL", "TP")];
                                                lines.push("-".repeat(60));
                                                let mut sorted: Vec<_> = strategies.iter().collect();
                                                sorted.sort_by_key(|(k, _)| (*k).clone());
                                                for (name, cfg) in sorted {
                                                    lines.push(format!(
                                                        "{:<12} {:<16} {:>5} {:>8} {:>8} {:>8}",
                                                        name,
                                                        cfg.symbol,
                                                        cfg.side,
                                                        cfg.size,
                                                        cfg.stop_loss.as_deref().unwrap_or("-"),
                                                        cfg.take_profit.as_deref().unwrap_or("-"),
                                                    ));
                                                }
                                                lines.join("\n")
                                            }
                                            Err(e) => format!("Failed to load strategies: {e}"),
                                        };
                                        state.command_output = Some(CommandOutput {
                                            command: "strategy list".to_string(),
                                            output,
                                            success: true,
                                            scroll_offset: 0,
                                        });
                                        state.input_mode = InputMode::CommandOutput;
                                    }
                                    Some(TuiCommand::StrategyRun { name }) => {
                                        match config::load_strategies() {
                                            Ok(strategies) => {
                                                if let Some(cfg) = strategies.get(&name) {
                                                    let side_str = cfg.side.to_uppercase();
                                                    let side = if side_str == "BUY"
                                                        || side_str == "BID"
                                                        || side_str == "LONG"
                                                    {
                                                        Side::Bid
                                                    } else {
                                                        Side::Ask
                                                    };
                                                    let qty: Decimal =
                                                        cfg.size.parse().unwrap_or(Decimal::ZERO);
                                                    state.pending_order_payload =
                                                        Some(ExecuteOrderPayload {
                                                            symbol: cfg.symbol.clone(),
                                                            side,
                                                            order_type: OrderType::Market,
                                                            time_in_force: Some(TimeInForce::IOC),
                                                            quantity: Some(qty),
                                                            ..Default::default()
                                                        });
                                                    let side_display = if side == Side::Bid {
                                                        "BUY"
                                                    } else {
                                                        "SELL"
                                                    };
                                                    state.input_mode = InputMode::ConfirmAction(
                                                        PendingAction::PlaceOrder {
                                                            symbol: cfg.symbol.clone(),
                                                            side: side_display.to_string(),
                                                            quantity: cfg.size.clone(),
                                                            price: None,
                                                        },
                                                    );
                                                } else {
                                                    state.set_notification(
                                                        format!("Strategy '{name}' not found"),
                                                        NotificationLevel::Error,
                                                    );
                                                    state.input_mode = InputMode::Normal;
                                                }
                                            }
                                            Err(e) => {
                                                state.set_notification(
                                                    format!("Failed to load strategies: {e}"),
                                                    NotificationLevel::Error,
                                                );
                                                state.input_mode = InputMode::Normal;
                                            }
                                        }
                                    }
                                    Some(TuiCommand::Stats) => {
                                        // Run as CLI passthrough: fills analytics --period 7d
                                        state.command_running =
                                            Some("fills analytics --period 7d".to_string());
                                        state.input_mode = InputMode::Normal;
                                        let tx = event_tx.clone();
                                        tokio::spawn(async move {
                                            let result = tokio::process::Command::new("bpx")
                                                .args(["fills", "analytics", "--period", "7d"])
                                                .stdout(std::process::Stdio::piped())
                                                .stderr(std::process::Stdio::piped())
                                                .output()
                                                .await;
                                            let (output, success) = match result {
                                                Ok(out) => {
                                                    let stdout =
                                                        String::from_utf8_lossy(&out.stdout);
                                                    let stderr =
                                                        String::from_utf8_lossy(&out.stderr);
                                                    let combined = if stderr.is_empty() {
                                                        stdout.to_string()
                                                    } else if stdout.is_empty() {
                                                        stderr.to_string()
                                                    } else {
                                                        format!("{stdout}\n{stderr}")
                                                    };
                                                    (combined, out.status.success())
                                                }
                                                Err(e) => (format!("Failed to run: {e}"), false),
                                            };
                                            let _ = tx.send(AppEvent::CommandResult {
                                                command: "fills analytics --period 7d".to_string(),
                                                output,
                                                success,
                                            });
                                        });
                                    }
                                    None if trade_cmd::parse_trade_command(&input).is_some() => {
                                        // Trade command (buy/sell/close/nuke/flatten)
                                        let trade = trade_cmd::parse_trade_command(&input).unwrap();
                                        if !state.authenticated {
                                            state.set_notification(
                                                "Not authenticated".to_string(),
                                                NotificationLevel::Error,
                                            );
                                            state.input_mode = InputMode::Normal;
                                        } else {
                                            match resolve_and_confirm(&trade, &mut state) {
                                                Ok(()) => {} // confirm dialog shown
                                                Err(msg) => {
                                                    state.set_notification(
                                                        msg,
                                                        NotificationLevel::Error,
                                                    );
                                                    state.input_mode = InputMode::Normal;
                                                }
                                            }
                                        }
                                    }
                                    None => {
                                        // CLI passthrough — check blocklist first
                                        let first_arg =
                                            input.split_whitespace().next().unwrap_or("");
                                        const BLOCKED: &[&str] = &["dashboard", "setup"];
                                        if BLOCKED.contains(&first_arg) {
                                            state.set_notification(
                                                format!(
                                                    "Cannot run '{first_arg}' from the dashboard"
                                                ),
                                                NotificationLevel::Error,
                                            );
                                            state.input_mode = InputMode::Normal;
                                        } else {
                                            // Show running indicator and execute
                                            state.command_running = Some(input.clone());
                                            state.input_mode = InputMode::Normal;
                                            let tx = event_tx.clone();
                                            tokio::spawn(async move {
                                                let args: Vec<&str> =
                                                    input.split_whitespace().collect();
                                                let result = tokio::process::Command::new("bpx")
                                                    .args(&args)
                                                    .stdout(std::process::Stdio::piped())
                                                    .stderr(std::process::Stdio::piped())
                                                    .output()
                                                    .await;
                                                let (output, success) = match result {
                                                    Ok(out) => {
                                                        let stdout =
                                                            String::from_utf8_lossy(&out.stdout);
                                                        let stderr =
                                                            String::from_utf8_lossy(&out.stderr);
                                                        let combined = if stderr.is_empty() {
                                                            stdout.to_string()
                                                        } else if stdout.is_empty() {
                                                            stderr.to_string()
                                                        } else {
                                                            format!("{stdout}\n{stderr}")
                                                        };
                                                        (combined, out.status.success())
                                                    }
                                                    Err(e) => {
                                                        (format!("Failed to run: {e}"), false)
                                                    }
                                                };
                                                let _ = tx.send(AppEvent::CommandResult {
                                                    command: input,
                                                    output,
                                                    success,
                                                });
                                            });
                                        }
                                    }
                                }
                            }
                            KeyCode::Backspace => {
                                if state.command_input.is_empty() {
                                    state.input_mode = InputMode::Normal;
                                    state.command_history_index = None;
                                } else {
                                    state.command_input.pop();
                                }
                            }
                            KeyCode::Up => {
                                if !state.command_history.is_empty() {
                                    let idx = match state.command_history_index {
                                        Some(i) => i.saturating_sub(1),
                                        None => state.command_history.len() - 1,
                                    };
                                    state.command_history_index = Some(idx);
                                    state.command_input = state.command_history[idx].clone();
                                }
                            }
                            KeyCode::Down => {
                                if let Some(idx) = state.command_history_index {
                                    if idx + 1 < state.command_history.len() {
                                        let new_idx = idx + 1;
                                        state.command_history_index = Some(new_idx);
                                        state.command_input =
                                            state.command_history[new_idx].clone();
                                    } else {
                                        state.command_history_index = None;
                                        state.command_input.clear();
                                    }
                                }
                            }
                            KeyCode::Char(c) => {
                                state.command_input.push(c);
                                state.command_history_index = None;
                            }
                            _ => {}
                        }
                        continue;
                    }

                    // Command output mode
                    if state.input_mode == InputMode::CommandOutput {
                        match key.code {
                            KeyCode::Char('q') | KeyCode::Esc | KeyCode::Enter => {
                                state.command_output = None;
                                state.input_mode = InputMode::Normal;
                            }
                            KeyCode::Char(':') => {
                                // Chain: close output and open new command bar
                                state.command_output = None;
                                state.input_mode = InputMode::CommandBar;
                                state.command_input.clear();
                                state.command_history_index = None;
                            }
                            KeyCode::Char('j') | KeyCode::Down => {
                                if let Some(ref mut output) = state.command_output {
                                    let max_scroll =
                                        output.output.lines().count().saturating_sub(1);
                                    if output.scroll_offset < max_scroll {
                                        output.scroll_offset += 1;
                                    }
                                }
                            }
                            KeyCode::Char('k') | KeyCode::Up => {
                                if let Some(ref mut output) = state.command_output {
                                    output.scroll_offset = output.scroll_offset.saturating_sub(1);
                                }
                            }
                            KeyCode::Char('g') => {
                                if let Some(ref mut output) = state.command_output {
                                    output.scroll_offset = 0;
                                }
                            }
                            KeyCode::Char('G') => {
                                if let Some(ref mut output) = state.command_output {
                                    output.scroll_offset =
                                        output.output.lines().count().saturating_sub(1);
                                }
                            }
                            _ => {}
                        }
                        continue;
                    }

                    // Normal mode — data-driven dispatch via keymap
                    // Esc is handled separately (contextual behavior)
                    if key.code == KeyCode::Esc {
                        if state.show_help {
                            state.show_help = false;
                        }
                    } else if let Some(action) =
                        state.keymap.lookup(key.code, key.modifiers).cloned()
                    {
                        match action {
                            Action::Quit => break 'main,
                            Action::ToggleHelp => state.show_help = !state.show_help,
                            Action::ToggleLatency => {
                                state.show_latency = !state.show_latency;
                            }
                            Action::TogglePnl => {
                                state.show_pnl = !state.show_pnl;
                            }
                            Action::ToggleCollateral => {
                                state.show_collateral = !state.show_collateral;
                            }
                            Action::SymbolSearch => {
                                state.input_mode = InputMode::SymbolSearch;
                                state.symbol_search_input.clear();
                                state.symbol_search_category = 0;
                                state.symbol_search_results = filter_markets(
                                    &state.markets,
                                    "",
                                    state.symbol_search_category,
                                );
                                state.symbol_search_selected = 0;
                            }
                            Action::ScrollDown => {
                                state.scroll_down(state.active_panel, 100);
                            }
                            Action::ScrollUp => {
                                state.scroll_up(state.active_panel);
                            }
                            Action::ScrollToTop => {
                                state.scroll_to_top(state.active_panel);
                            }
                            Action::ScrollToBottom => {
                                state.scroll_to_bottom(state.active_panel, 100);
                            }
                            Action::CancelOrder => {
                                if state.active_panel == Panel::Orders
                                    && state.authenticated
                                    && !state.open_orders.is_empty()
                                {
                                    let scroll = state.scroll_offset(Panel::Orders);
                                    if let Some(order) = state.open_orders.get(scroll) {
                                        let symbol = order
                                            .get("symbol")
                                            .or_else(|| order.get("s"))
                                            .and_then(|v| v.as_str())
                                            .unwrap_or("")
                                            .to_string();
                                        let order_id = order
                                            .get("id")
                                            .and_then(|v| v.as_str())
                                            .unwrap_or("")
                                            .to_string();
                                        if !order_id.is_empty() {
                                            state.input_mode = InputMode::ConfirmAction(
                                                PendingAction::CancelOrder { symbol, order_id },
                                            );
                                        }
                                    }
                                }
                            }
                            Action::ClosePosition => {
                                if state.active_panel == Panel::Positions
                                    && state.authenticated
                                    && !state.positions.is_empty()
                                {
                                    let scroll = state.scroll_offset(Panel::Positions);
                                    if let Some(pos) = state.positions.get(scroll) {
                                        let symbol = pos
                                            .get("symbol")
                                            .and_then(|v| v.as_str())
                                            .unwrap_or("")
                                            .to_string();
                                        let qty_str = pos
                                            .get("netQuantity")
                                            .or_else(|| pos.get("net_quantity"))
                                            .and_then(|v| v.as_str())
                                            .unwrap_or("0");
                                        let qty: f64 = qty_str.parse().unwrap_or(0.0);
                                        if qty != 0.0 {
                                            let side = if qty >= 0.0 {
                                                "LONG".to_string()
                                            } else {
                                                "SHORT".to_string()
                                            };
                                            let abs_qty = format!("{:.8}", qty.abs());
                                            state.input_mode = InputMode::ConfirmAction(
                                                PendingAction::ClosePosition {
                                                    symbol,
                                                    side,
                                                    quantity: abs_qty,
                                                },
                                            );
                                        }
                                    }
                                }
                            }
                            Action::OpenOrderForm => {
                                if state.authenticated {
                                    let best_ask = state
                                        .orderbook
                                        .asks
                                        .keys()
                                        .next()
                                        .map(|p| p.to_string())
                                        .unwrap_or_default();
                                    state.order_form = Some(OrderFormState {
                                        symbol: state.symbol.clone(),
                                        side: Side::Bid,
                                        price_input: best_ask,
                                        quantity_input: String::new(),
                                        post_only: false,
                                        time_in_force: TimeInForce::GTC,
                                        reduce_only: false,
                                        active_field: OrderFormField::Quantity,
                                        error_message: None,
                                    });
                                    state.input_mode = InputMode::OrderEntry;
                                }
                            }
                            Action::ChartIntervalPrev => {
                                if state.kline_interval > 0 {
                                    state.kline_interval -= 1;
                                    state.kline_history.clear();
                                    let interval = state.current_kline_interval();
                                    let _ = params_tx.send(ws::SubscriptionParams {
                                        symbol: state.symbol.clone(),
                                        kline_interval: interval.to_string(),
                                    });
                                    if let Some(ref c) = public_client {
                                        let window = kline_window_secs(interval);
                                        let start = std::time::SystemTime::now()
                                            .duration_since(std::time::UNIX_EPOCH)
                                            .unwrap()
                                            .as_secs()
                                            as i64
                                            - window;
                                        if let Ok(klines) = c
                                            .get_k_lines(&state.symbol, interval, start, None)
                                            .await
                                        {
                                            for k in
                                                klines.iter().rev().take(MAX_KLINE_POINTS).rev()
                                            {
                                                if let Some(close) = k.close {
                                                    if let Ok(f) = close.to_string().parse::<f64>()
                                                    {
                                                        state.kline_history.push_back(f);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            Action::ChartIntervalNext => {
                                if state.kline_interval < KLINE_INTERVALS.len() - 1 {
                                    state.kline_interval += 1;
                                    state.kline_history.clear();
                                    let interval = state.current_kline_interval();
                                    let _ = params_tx.send(ws::SubscriptionParams {
                                        symbol: state.symbol.clone(),
                                        kline_interval: interval.to_string(),
                                    });
                                    if let Some(ref c) = public_client {
                                        let window = kline_window_secs(interval);
                                        let start = std::time::SystemTime::now()
                                            .duration_since(std::time::UNIX_EPOCH)
                                            .unwrap()
                                            .as_secs()
                                            as i64
                                            - window;
                                        if let Ok(klines) = c
                                            .get_k_lines(&state.symbol, interval, start, None)
                                            .await
                                        {
                                            for k in
                                                klines.iter().rev().take(MAX_KLINE_POINTS).rev()
                                            {
                                                if let Some(close) = k.close {
                                                    if let Ok(f) = close.to_string().parse::<f64>()
                                                    {
                                                        state.kline_history.push_back(f);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                            Action::NextPanel => {
                                state.active_panel = state.active_panel.next();
                            }
                            Action::PrevPanel => {
                                state.active_panel = state.active_panel.prev();
                            }
                            Action::JumpPanel(n) => {
                                state.active_panel = match n {
                                    1 => Panel::OrderBook,
                                    2 => Panel::Trades,
                                    3 => Panel::Funding,
                                    4 => Panel::Orders,
                                    5 => Panel::Positions,
                                    6 => Panel::Balances,
                                    _ => state.active_panel,
                                };
                            }
                            Action::CommandBar => {
                                state.input_mode = InputMode::CommandBar;
                                state.command_input.clear();
                                state.command_history_index = None;
                            }
                            Action::RunCommand(cmd) => {
                                // Execute a pre-bound command directly
                                let cmd = cmd.clone();
                                state.command_running = Some(cmd.clone());
                                let tx = event_tx.clone();
                                tokio::spawn(async move {
                                    let args: Vec<&str> = cmd.split_whitespace().collect();
                                    let result = tokio::process::Command::new("bpx")
                                        .args(&args)
                                        .stdout(std::process::Stdio::piped())
                                        .stderr(std::process::Stdio::piped())
                                        .output()
                                        .await;
                                    let (output, success) = match result {
                                        Ok(out) => {
                                            let stdout = String::from_utf8_lossy(&out.stdout);
                                            let stderr = String::from_utf8_lossy(&out.stderr);
                                            let combined = if stderr.is_empty() {
                                                stdout.to_string()
                                            } else if stdout.is_empty() {
                                                stderr.to_string()
                                            } else {
                                                format!("{stdout}\n{stderr}")
                                            };
                                            (combined, out.status.success())
                                        }
                                        Err(e) => (format!("Failed to run: {e}"), false),
                                    };
                                    let _ = tx.send(AppEvent::CommandResult {
                                        command: cmd,
                                        output,
                                        success,
                                    });
                                });
                            }
                        }
                    }
                }
                AppEvent::Tick => {
                    // Auto-dismiss notifications
                    state.tick_notification();

                    // Collect unrealized PnL from positions for sparkline
                    // Always collect so there's history when user toggles PnL view on
                    if !state.positions.is_empty() {
                        let total_pnl: f64 = state
                            .positions
                            .iter()
                            .filter_map(|pos| {
                                pos.get("unrealizedPnl")
                                    .or_else(|| pos.get("unrealized_pnl"))
                                    .or_else(|| pos.get("pnl"))
                                    .and_then(|v| {
                                        v.as_str()
                                            .and_then(|s| s.parse::<f64>().ok())
                                            .or_else(|| v.as_f64())
                                    })
                            })
                            .sum();
                        state.pnl_history.push(total_pnl);
                        if state.pnl_history.len() > MAX_PNL_SAMPLES {
                            state.pnl_history.remove(0);
                        }
                    }

                    // Periodic funding snapshot recording & trend cache update
                    let should_snapshot = state
                        .last_funding_snapshot
                        .map(|t| t.elapsed() >= FUNDING_SNAPSHOT_INTERVAL)
                        .unwrap_or(true);
                    if should_snapshot && !state.all_mark_prices.is_empty() {
                        let now_ts = std::time::SystemTime::now()
                            .duration_since(std::time::UNIX_EPOCH)
                            .unwrap_or_default()
                            .as_secs() as i64;

                        let mut snapshots = Vec::new();
                        for mp in &state.all_mark_prices {
                            if let (Some(sym), Some(rate)) = (
                                mp.get("symbol").and_then(|v| v.as_str()),
                                mp.get("fundingRate")
                                    .or_else(|| mp.get("funding_rate"))
                                    .and_then(|v| {
                                        v.as_str()
                                            .and_then(|s| s.parse::<f64>().ok())
                                            .or_else(|| v.as_f64())
                                    }),
                            ) {
                                snapshots.push(crate::history::FundingSnapshot {
                                    timestamp: now_ts,
                                    symbol: sym.to_string(),
                                    funding_rate: rate,
                                });
                                // Update in-memory trend cache
                                let cache = state
                                    .funding_trend_cache
                                    .entry(sym.to_string())
                                    .or_default();
                                cache.push(rate);
                                if cache.len() > 24 {
                                    cache.remove(0);
                                }
                            }
                        }
                        // Write to disk in background (non-blocking)
                        if !snapshots.is_empty() {
                            tokio::task::spawn_blocking(move || {
                                let _ = crate::history::append_snapshots(&snapshots);
                            });
                        }
                        state.last_funding_snapshot = Some(Instant::now());
                    }

                    // Periodic REST refresh for private data
                    if state.authenticated
                        && last_private_refresh.elapsed() >= PRIVATE_REFRESH_INTERVAL
                    {
                        if let Some(ref c) = auth_client {
                            seed_private_data(&mut state, c).await;
                        }
                        last_private_refresh = Instant::now();

                        // Basis trade detection: short perp + spot holding
                        state.basis_symbols.clear();
                        for pos in &state.positions {
                            let qty: f64 = pos
                                .get("netQuantity")
                                .or_else(|| pos.get("net_quantity"))
                                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                                .unwrap_or(0.0);
                            if qty < 0.0 {
                                if let Some(sym) = pos.get("symbol").and_then(|v| v.as_str()) {
                                    let base = sym.split('_').next().unwrap_or("");
                                    let has_spot = state
                                        .balances
                                        .get(base)
                                        .and_then(|b| {
                                            b.get("total").and_then(|v| {
                                                v.as_str().and_then(|s| s.parse::<f64>().ok())
                                            })
                                        })
                                        .unwrap_or(0.0)
                                        > 0.0;
                                    if has_spot {
                                        state.basis_symbols.insert(sym.to_string());
                                    }
                                }
                            }
                        }

                        // Risk analytics: HHI + largest position
                        compute_risk_analytics(&mut state);

                        // Optimization hints
                        compute_optimization_hints(&mut state);
                    }
                }
                AppEvent::ActionResult(result) => {
                    match &result {
                        ActionResult::OrderPlaced {
                            symbol,
                            side,
                            qty,
                            price,
                        } => {
                            let price_info = price
                                .as_ref()
                                .map(|p| format!(" @ {p}"))
                                .unwrap_or_else(|| " (market)".to_string());
                            state.set_notification(
                                format!("{side} {qty} {symbol}{price_info}"),
                                NotificationLevel::Success,
                            );
                        }
                        ActionResult::OrderCancelled { symbol, order_id } => {
                            let short_id = if order_id.len() > 8 {
                                &order_id[..8]
                            } else {
                                order_id
                            };
                            state.set_notification(
                                format!("Cancelled {short_id}.. on {symbol}"),
                                NotificationLevel::Success,
                            );
                        }
                        ActionResult::OrderFailed { message } => {
                            state.set_notification(
                                format!("Order failed: {message}"),
                                NotificationLevel::Error,
                            );
                        }
                        ActionResult::PositionClosed { symbol } => {
                            state.set_notification(
                                format!("Closed position on {symbol}"),
                                NotificationLevel::Success,
                            );
                        }
                        ActionResult::PositionCloseFailed { message } => {
                            state.set_notification(
                                format!("Close failed: {message}"),
                                NotificationLevel::Error,
                            );
                        }
                        ActionResult::OrdersCancelled { symbol, count } => {
                            state.set_notification(
                                format!("Cancelled {count} orders on {symbol}"),
                                NotificationLevel::Success,
                            );
                        }
                        ActionResult::OrdersCancelFailed { message } => {
                            state.set_notification(
                                format!("Cancel failed: {message}"),
                                NotificationLevel::Error,
                            );
                        }
                        ActionResult::NukeCompleted {
                            orders_cancelled,
                            positions_closed,
                        } => {
                            state.set_notification(
                                format!(
                                    "Nuked: {orders_cancelled} orders cancelled, {positions_closed} positions closed"
                                ),
                                NotificationLevel::Success,
                            );
                        }
                        ActionResult::NukeFailed { message } => {
                            state.set_notification(
                                format!("Nuke failed: {message}"),
                                NotificationLevel::Error,
                            );
                        }
                    }

                    // Trigger REST refresh after any action
                    if let Some(ref c) = auth_client {
                        seed_private_data(&mut state, c).await;
                    }
                    last_private_refresh = Instant::now();
                }
                AppEvent::CommandResult {
                    command,
                    output,
                    success,
                } => {
                    state.command_running = None;
                    // Short output (1 line) → notification, longer → drawer
                    let line_count = output.lines().count();
                    if line_count <= 1 && success {
                        let msg = output.trim().to_string();
                        if !msg.is_empty() {
                            state.set_notification(msg, NotificationLevel::Info);
                        }
                    } else {
                        state.command_output = Some(CommandOutput {
                            command,
                            output,
                            success,
                            scroll_offset: 0,
                        });
                        state.input_mode = InputMode::CommandOutput;
                    }
                }
                AppEvent::WsUpdate(msg) => {
                    state.ws_last_update = Some(Instant::now());
                    match msg {
                        WsMessage::Ticker(data) => {
                            state.ws_connected = true;
                            state.ticker = Some(data);
                        }
                        WsMessage::Depth(data) => {
                            state.orderbook.apply_update(&data);
                        }
                        WsMessage::Trade(data) => {
                            state.recent_trades.push_front(data);
                            if state.recent_trades.len() > MAX_RECENT_TRADES {
                                state.recent_trades.pop_back();
                            }
                        }
                        WsMessage::Kline(data) => {
                            if let Some(close) = data
                                .get("c")
                                .or_else(|| data.get("close"))
                                .and_then(|v| v.as_str())
                                .and_then(|s| s.parse::<f64>().ok())
                            {
                                state.kline_history.push_back(close);
                                if state.kline_history.len() > MAX_KLINE_POINTS {
                                    state.kline_history.pop_front();
                                }
                            }
                        }
                        WsMessage::MarkPrice(data) => {
                            state.mark_price = Some(data);
                        }
                        WsMessage::OrderUpdate(_data) => {
                            // Private WS order update — trigger REST refresh
                            if let Some(ref c) = auth_client {
                                if let Ok(orders) = c.get_open_orders(None).await {
                                    state.open_orders = orders
                                        .iter()
                                        .filter_map(|o| serde_json::to_value(o).ok())
                                        .collect();
                                }
                            }
                        }
                        WsMessage::BalanceUpdate(_data) => {
                            // Private WS balance update — trigger REST refresh
                            if let Some(ref c) = auth_client {
                                if let Ok(balances) = c.get_balances().await {
                                    state.balances.clear();
                                    for (asset, bal) in &balances {
                                        if let Ok(v) = serde_json::to_value(bal) {
                                            state.balances.insert(asset.clone(), v);
                                        }
                                    }
                                }
                            }
                        }
                        WsMessage::LatencySample(sample) => {
                            state.latency_history.push_back(sample);
                            while state.latency_history.len() > MAX_LATENCY_SAMPLES {
                                state.latency_history.pop_front();
                            }
                        }
                        WsMessage::PingRtt(rtt) => {
                            let rtt_us = rtt.as_micros() as u64;
                            state.ping_rtt = Some(rtt);
                            state.ping_history.push_back(rtt_us);
                            while state.ping_history.len() > MAX_PING_SAMPLES {
                                state.ping_history.pop_front();
                            }
                        }
                        WsMessage::Connected => {
                            state.ws_connected = true;
                            state.ws_reconnect_count = 0;
                        }
                        WsMessage::Disconnected => {
                            state.ws_connected = false;
                        }
                        WsMessage::Reconnecting(count) => {
                            state.ws_connected = false;
                            state.ws_reconnect_count = count;
                        }
                        WsMessage::PrivateConnected => {
                            state.private_ws_connected = true;
                        }
                        WsMessage::PrivateDisconnected => {
                            state.private_ws_connected = false;
                        }
                        WsMessage::PrivateReconnecting(_) => {
                            state.private_ws_connected = false;
                        }
                    }
                }
                AppEvent::Resize(_, _) => {
                    // Terminal will be redrawn on next loop iteration
                }
            }
        }
    }

    // Restore terminal
    disable_raw_mode()?;
    io::stdout().execute(LeaveAlternateScreen)?;
    terminal.show_cursor()?;

    Ok(())
}

/// Fetch private data via REST and populate state.
async fn seed_private_data(state: &mut AppState, c: &bpx_api_client::BpxClient) {
    // Open orders
    if let Ok(orders) = c.get_open_orders(None).await {
        state.open_orders = orders
            .iter()
            .filter_map(|o| serde_json::to_value(o).ok())
            .collect();
    }
    // Balances
    if let Ok(balances) = c.get_balances().await {
        state.balances.clear();
        for (asset, bal) in &balances {
            if let Ok(v) = serde_json::to_value(bal) {
                state.balances.insert(asset.clone(), v);
            }
        }
    }
    // Positions
    if let Ok(positions) = c.get_open_future_positions().await {
        state.positions = positions
            .iter()
            .filter_map(|p| serde_json::to_value(p).ok())
            .collect();
    }
    // Collateral (margin health, IMF, MMF)
    if let Ok(coll) = c.get_collateral().await {
        state.collateral = serde_json::to_value(coll).ok();
    }
    // Borrow/lend positions
    if let Ok(positions) = c.get_borrow_lend_positions().await {
        state.borrow_lend_positions = positions
            .iter()
            .filter_map(|p| serde_json::to_value(p).ok())
            .collect();
    }
}

/// Compute a what-if scenario from command args.
/// Supports: "buy 10 SOL", "sell 5 BTC", "move -10" (price move %).
fn compute_scenario(args: &str, state: &AppState) -> String {
    let parts: Vec<&str> = args.split_whitespace().collect();
    if parts.is_empty() {
        return "Usage:\n  :scenario buy 10 SOL   - trade scenario\n  :scenario sell 5 BTC   - trade scenario\n  :scenario move -10     - price move %".to_string();
    }

    match parts[0].to_lowercase().as_str() {
        "buy" | "sell" => {
            if parts.len() < 3 {
                return "Usage: :scenario buy/sell <qty> <asset>".to_string();
            }
            let qty: f64 = match parts[1].parse() {
                Ok(q) => q,
                Err(_) => return format!("Invalid quantity: {}", parts[1]),
            };
            let asset = parts[2].to_uppercase();
            let is_buy = parts[0].to_lowercase() == "buy";
            let perp_sym = format!("{asset}_USDC_PERP");

            // Get current mark price
            let mark: f64 = state
                .all_mark_prices
                .iter()
                .find(|mp| mp.get("symbol").and_then(|v| v.as_str()) == Some(&perp_sym))
                .and_then(|mp| {
                    mp.get("markPrice")
                        .or_else(|| mp.get("mark_price"))
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
                })
                .unwrap_or(0.0);

            if mark == 0.0 {
                return format!("No mark price found for {perp_sym}");
            }

            let trade_notional = qty * mark;

            // Current state
            let current_exposure: f64 = state
                .positions
                .iter()
                .map(|p| {
                    let q: f64 = p
                        .get("netQuantity")
                        .or_else(|| p.get("net_quantity"))
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                        .unwrap_or(0.0);
                    let m: f64 = p
                        .get("markPrice")
                        .or_else(|| p.get("mark_price"))
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                        .unwrap_or(0.0);
                    q.abs() * m
                })
                .sum();

            let margin_fraction: f64 = state
                .collateral
                .as_ref()
                .and_then(|c| {
                    c.get("marginFraction")
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
                })
                .unwrap_or(0.0);
            let net_equity: f64 = state
                .collateral
                .as_ref()
                .and_then(|c| {
                    c.get("netEquity")
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
                })
                .unwrap_or(0.0);

            let new_exposure = if is_buy {
                current_exposure + trade_notional
            } else {
                (current_exposure - trade_notional).max(0.0)
            };

            // Estimate new margin fraction (simplified: equity / exposure)
            let new_margin = if new_exposure > 0.0 {
                net_equity / new_exposure
            } else {
                f64::INFINITY
            };

            let side_label = if is_buy { "BUY" } else { "SELL" };
            format!(
                "Scenario: {side_label} {qty} {asset} @ ${mark:.2}\n\
                 Trade notional: ${trade_notional:.2}\n\n\
                 {:<20} {:>12} {:>12}\n\
                 {:-<46}\n\
                 {:<20} {:>12} {:>12}\n\
                 {:<20} {:>12} {:>12}",
                "", "BEFORE", "AFTER",
                "",
                "Total Exposure",
                format!("${current_exposure:.0}"),
                format!("${new_exposure:.0}"),
                "Margin Fraction",
                if margin_fraction > 0.0 { format!("{:.1}%", margin_fraction * 100.0) } else { "-".to_string() },
                if new_margin.is_finite() { format!("{:.1}%", new_margin * 100.0) } else { "-".to_string() },
            )
        }
        "move" => {
            if parts.len() < 2 {
                return "Usage: :scenario move <pct> (e.g., move -10)".to_string();
            }
            let pct: f64 = match parts[1].trim_end_matches('%').parse() {
                Ok(p) => p,
                Err(_) => return format!("Invalid percentage: {}", parts[1]),
            };
            let factor = pct / 100.0;

            let mut lines = vec![format!("Scenario: All prices move {pct:+.1}%\n")];
            lines.push(format!(
                "{:<14} {:>8} {:>10} {:>10}",
                "SYMBOL", "QTY", "CUR PnL", "NEW PnL"
            ));
            lines.push("-".repeat(46));

            let mut total_current_pnl = 0.0_f64;
            let mut total_new_pnl = 0.0_f64;

            for pos in &state.positions {
                let sym = pos
                    .get("symbol")
                    .and_then(|v| v.as_str())
                    .unwrap_or("-");
                let qty: f64 = pos
                    .get("netQuantity")
                    .or_else(|| pos.get("net_quantity"))
                    .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                    .unwrap_or(0.0);
                let entry: f64 = pos
                    .get("entryPrice")
                    .or_else(|| pos.get("entry_price"))
                    .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                    .unwrap_or(0.0);
                let mark: f64 = pos
                    .get("markPrice")
                    .or_else(|| pos.get("mark_price"))
                    .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                    .unwrap_or(0.0);

                if mark == 0.0 || entry == 0.0 {
                    continue;
                }

                let current_pnl = qty * (mark - entry);
                let new_mark = mark * (1.0 + factor);
                let new_pnl = qty * (new_mark - entry);
                total_current_pnl += current_pnl;
                total_new_pnl += new_pnl;

                let display_sym = sym.replace("_USDC_PERP", "_PP").replace("_USDC", "");
                lines.push(format!(
                    "{:<14} {:>8.2} {:>+10.2} {:>+10.2}",
                    display_sym, qty, current_pnl, new_pnl
                ));
            }

            lines.push("-".repeat(46));
            lines.push(format!(
                "{:<14} {:>8} {:>+10.2} {:>+10.2}",
                "TOTAL", "", total_current_pnl, total_new_pnl
            ));
            let delta = total_new_pnl - total_current_pnl;
            lines.push(format!("\nPnL impact: {:+.2}", delta));
            lines.join("\n")
        }
        _ => "Usage:\n  :scenario buy 10 SOL   - trade scenario\n  :scenario sell 5 BTC   - trade scenario\n  :scenario move -10     - price move %".to_string(),
    }
}

/// Compute risk summary report with stress tests.
fn compute_risk_report(state: &AppState) -> String {
    let mut lines: Vec<String> = Vec::new();

    // Margin utilization
    let margin_fraction: f64 = state
        .collateral
        .as_ref()
        .and_then(|c| {
            c.get("marginFraction")
                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
        })
        .unwrap_or(0.0);
    let mmf: f64 = state
        .collateral
        .as_ref()
        .and_then(|c| {
            c.get("mmf")
                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
        })
        .unwrap_or(0.0);
    let net_equity: f64 = state
        .collateral
        .as_ref()
        .and_then(|c| {
            c.get("netEquity")
                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
        })
        .unwrap_or(0.0);

    let util_pct = if mmf > 0.0 && margin_fraction > 0.0 {
        (mmf / margin_fraction) * 100.0
    } else {
        0.0
    };

    lines.push("Risk Summary".to_string());
    lines.push("=".repeat(40));
    lines.push(format!("Net Equity:        ${net_equity:.2}"));
    lines.push(format!(
        "Margin Fraction:   {}",
        if margin_fraction > 0.0 {
            format!("{:.1}%", margin_fraction * 100.0)
        } else {
            "-".to_string()
        }
    ));
    lines.push(format!(
        "Margin Utilization: {}",
        if util_pct > 0.0 {
            format!("{util_pct:.1}%")
        } else {
            "-".to_string()
        }
    ));

    // Total exposure
    let total_exposure: f64 = state
        .positions
        .iter()
        .map(|p| {
            let q: f64 = p
                .get("netQuantity")
                .or_else(|| p.get("net_quantity"))
                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                .unwrap_or(0.0);
            let m: f64 = p
                .get("markPrice")
                .or_else(|| p.get("mark_price"))
                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                .unwrap_or(0.0);
            q.abs() * m
        })
        .sum();
    lines.push(format!("Total Exposure:    ${total_exposure:.0}"));

    // HHI
    if let Some(hhi) = state.risk_hhi {
        let label = if hhi < 2500.0 {
            "Diversified"
        } else if hhi < 5000.0 {
            "Moderate"
        } else {
            "Concentrated"
        };
        lines.push(format!("HHI:               {hhi:.0} ({label})"));
    }

    // Largest position
    if let Some((ref sym, pct)) = state.risk_largest_pct {
        let display_sym = sym.replace("_USDC_PERP", "_PP").replace("_USDC", "");
        lines.push(format!("Largest Position:  {display_sym} ({pct:.0}%)"));
    }

    // Stress tests
    lines.push(String::new());
    lines.push("Stress Tests".to_string());
    lines.push("-".repeat(40));
    lines.push(format!(
        "{:<20} {:>10} {:>8}",
        "SCENARIO", "MARGIN%", "LIQ?"
    ));

    let scenarios = [
        ("BTC -10%", "BTC", -0.10),
        ("BTC -25%", "BTC", -0.25),
        ("All -20%", "", -0.20),
    ];

    for (label, asset, factor) in &scenarios {
        let mut stress_pnl = 0.0_f64;
        let mut stressed_exposure = 0.0_f64;
        for pos in &state.positions {
            let sym = pos.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
            let qty: f64 = pos
                .get("netQuantity")
                .or_else(|| pos.get("net_quantity"))
                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                .unwrap_or(0.0);
            let mark: f64 = pos
                .get("markPrice")
                .or_else(|| pos.get("mark_price"))
                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                .unwrap_or(0.0);
            let is_target = asset.is_empty() || sym.starts_with(asset);
            let new_mark = if is_target {
                mark * (1.0 + factor)
            } else {
                mark
            };
            if is_target {
                stress_pnl += qty * mark * factor;
            }
            stressed_exposure += qty.abs() * new_mark;
        }

        let stressed_equity = net_equity + stress_pnl;
        let stressed_margin = if stressed_exposure > 0.0 {
            stressed_equity / stressed_exposure
        } else {
            f64::INFINITY
        };
        let liq_flag = if mmf > 0.0 && stressed_margin < mmf {
            "!! LIQ"
        } else if mmf > 0.0 && stressed_margin < mmf * 1.5 {
            "DANGER"
        } else {
            "OK"
        };
        lines.push(format!(
            "{:<20} {:>10} {:>8}",
            label,
            if stressed_margin.is_finite() {
                format!("{:.1}%", stressed_margin * 100.0)
            } else {
                "-".to_string()
            },
            liq_flag,
        ));
    }

    lines.join("\n")
}

/// Compute HHI concentration index and largest position for risk display.
fn compute_risk_analytics(state: &mut AppState) {
    if state.positions.is_empty() {
        state.risk_hhi = None;
        state.risk_largest_pct = None;
        return;
    }

    let mut notionals: Vec<(String, f64)> = Vec::new();
    let mut total_notional = 0.0_f64;

    for pos in &state.positions {
        let sym = pos
            .get("symbol")
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();
        let qty: f64 = pos
            .get("netQuantity")
            .or_else(|| pos.get("net_quantity"))
            .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
            .unwrap_or(0.0);
        let mark: f64 = pos
            .get("markPrice")
            .or_else(|| pos.get("mark_price"))
            .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
            .unwrap_or(0.0);
        let notional = qty.abs() * mark;
        if notional > 0.0 {
            total_notional += notional;
            notionals.push((sym, notional));
        }
    }

    if total_notional > 0.0 {
        // HHI: sum of (share%)^2, scale 0-10000
        let hhi: f64 = notionals
            .iter()
            .map(|(_, n)| {
                let share = (n / total_notional) * 100.0;
                share * share
            })
            .sum();
        state.risk_hhi = Some(hhi);

        // Largest position
        if let Some((sym, n)) = notionals
            .iter()
            .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
        {
            state.risk_largest_pct = Some((sym.clone(), (n / total_notional) * 100.0));
        }
    } else {
        state.risk_hhi = None;
        state.risk_largest_pct = None;
    }
}

/// Count optimization hints from current state.
fn compute_optimization_hints(state: &mut AppState) {
    let mut count = 0u32;

    // Count idle balances: non-USDC with available > 0 and no lending position
    let lending_symbols: HashSet<&str> = state
        .borrow_lend_positions
        .iter()
        .filter_map(|p| {
            let qty: f64 = p
                .get("netQuantity")
                .or_else(|| p.get("net_quantity"))
                .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
                .unwrap_or(0.0);
            if qty > 0.0 {
                p.get("symbol").and_then(|v| v.as_str())
            } else {
                None
            }
        })
        .collect();

    for (asset, bal) in &state.balances {
        if asset == "USDC" {
            continue;
        }
        let available: f64 = bal
            .get("available")
            .and_then(|v| v.as_str().and_then(|s| s.parse().ok()))
            .unwrap_or(0.0);
        if available > 0.0 && !lending_symbols.contains(asset.as_str()) {
            count += 1;
        }
    }

    // Count margin utilization > 80%
    if let Some(ref coll) = state.collateral {
        let mmf: f64 = coll
            .get("mmf")
            .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
            .unwrap_or(0.0);
        let mf: f64 = coll
            .get("marginFraction")
            .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
            .unwrap_or(0.0);
        if mmf > 0.0 && mf > 0.0 {
            let util = (mmf / mf) * 100.0;
            if util > 80.0 {
                count += 1;
            }
        }
    }

    // Count collateral concentration > 50% for non-USDC
    if let Some(ref coll) = state.collateral {
        if let Some(items) = coll.get("collateral").and_then(|v| v.as_array()) {
            let total_value: f64 = items
                .iter()
                .filter_map(|item| {
                    item.get("collateralValue")
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
                })
                .sum();
            if total_value > 0.0 {
                for item in items {
                    let sym = item.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
                    if sym == "USDC" {
                        continue;
                    }
                    let value: f64 = item
                        .get("collateralValue")
                        .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or(v.as_f64()))
                        .unwrap_or(0.0);
                    if value / total_value > 0.50 {
                        count += 1;
                    }
                }
            }
        }
    }

    state.optimization_hint_count = count;
}

/// Fetch open interest data from the REST API (not in SDK yet).
async fn fetch_open_interest(
    state: &mut AppState,
    client: &bpx_api_client::BpxClient,
    base_url: Option<&str>,
    config: &crate::config::ResolvedConfig,
) {
    let api_base = crate::auth::resolve_base_url(base_url, config);
    let url = format!("{}/api/v1/openInterest", api_base.trim_end_matches('/'));
    let result: Result<Vec<serde_json::Value>, _> = async {
        let resp = std::ops::Deref::deref(client).get(&url).send().await?;
        resp.json().await
    }
    .await;
    if let Ok(items) = result {
        for item in items {
            if let Some(sym) = item.get("symbol").and_then(|s| s.as_str()) {
                state.open_interest.insert(sym.to_string(), item);
            }
        }
    }
}

/// Fetch borrow/lend market rates from the REST API (not in SDK yet).
async fn fetch_borrow_lend_markets(
    state: &mut AppState,
    client: &bpx_api_client::BpxClient,
    base_url: Option<&str>,
    config: &crate::config::ResolvedConfig,
) {
    let api_base = crate::auth::resolve_base_url(base_url, config);
    let url = format!(
        "{}/api/v1/borrowLend/markets",
        api_base.trim_end_matches('/')
    );
    let result: Result<Vec<serde_json::Value>, _> = async {
        let resp = std::ops::Deref::deref(client).get(&url).send().await?;
        resp.json().await
    }
    .await;
    if let Ok(items) = result {
        for item in items {
            if let Some(sym) = item.get("symbol").and_then(|s| s.as_str()) {
                state.borrow_lend_markets.insert(sym.to_string(), item);
            }
        }
    }
}

/// Resolve a parsed trade command against live state and set up the confirm dialog.
fn resolve_and_confirm(cmd: &TradeCommand, state: &mut AppState) -> Result<(), String> {
    let symbol = &state.symbol;

    // Extract mark price from WS or REST data
    let mark_price = state
        .mark_price
        .as_ref()
        .and_then(|mp| {
            mp.get("p")
                .or_else(|| mp.get("markPrice"))
                .or_else(|| mp.get("mark_price"))
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<Decimal>().ok())
        })
        .or_else(|| {
            state
                .all_mark_prices
                .iter()
                .find(|v| {
                    v.get("symbol")
                        .and_then(|s| s.as_str())
                        .is_some_and(|s| s == symbol.as_str())
                })
                .and_then(|v| {
                    v.get("markPrice")
                        .or_else(|| v.get("mark_price"))
                        .and_then(|v| v.as_str())
                        .and_then(|s| s.parse::<Decimal>().ok())
                })
        });

    let best_bid = state.orderbook.bids.keys().next_back().copied();
    let best_ask = state.orderbook.asks.keys().next().copied();

    // Find current position on this symbol
    let position = state.positions.iter().find_map(|pos| {
        let pos_sym = pos.get("symbol").and_then(|v| v.as_str()).unwrap_or("");
        if pos_sym != symbol.as_str() {
            return None;
        }
        let qty_str = pos
            .get("netQuantity")
            .or_else(|| pos.get("net_quantity"))
            .and_then(|v| v.as_str())
            .unwrap_or("0");
        let qty: Decimal = qty_str.parse().ok()?;
        if qty.is_zero() {
            return None;
        }
        let side = if qty.is_sign_positive() {
            Side::Bid
        } else {
            Side::Ask
        };
        Some((side, qty.abs()))
    });

    let resolved = trade_cmd::resolve_trade_command(
        cmd, symbol, mark_price, best_bid, best_ask, position,
        None, // max_order_qty — not cached; % size will error
    )?;

    match resolved {
        ResolvedAction::PlaceOrder(order) => {
            let side_str = if order.payload.side == Side::Bid {
                "BUY"
            } else {
                "SELL"
            };
            let qty_str = order
                .payload
                .quantity
                .map(|q| q.to_string())
                .unwrap_or_default();
            let price_str = order.payload.price.map(|p| p.to_string());
            state.pending_order_payload = Some(order.payload);
            state.input_mode = InputMode::ConfirmAction(PendingAction::PlaceOrder {
                symbol: symbol.clone(),
                side: side_str.to_string(),
                quantity: qty_str,
                price: price_str,
            });
        }
        ResolvedAction::ClosePosition(order) => {
            let close_side = order.payload.side;
            let pos_side_str = if close_side == Side::Ask {
                "LONG"
            } else {
                "SHORT"
            };
            let qty_str = order
                .payload
                .quantity
                .map(|q| q.to_string())
                .unwrap_or_default();
            state.pending_order_payload = Some(order.payload);
            state.input_mode = InputMode::ConfirmAction(PendingAction::PlaceOrder {
                symbol: symbol.clone(),
                side: format!("Close {pos_side_str}"),
                quantity: qty_str,
                price: None,
            });
        }
        ResolvedAction::NukeOrders => {
            let count = state
                .open_orders
                .iter()
                .filter(|o| {
                    o.get("symbol")
                        .or_else(|| o.get("s"))
                        .and_then(|v| v.as_str())
                        .is_some_and(|s| s == symbol.as_str())
                })
                .count();
            if count == 0 {
                return Err(format!("No open orders on {symbol}"));
            }
            state.input_mode = InputMode::ConfirmAction(PendingAction::NukeOrders {
                symbol: symbol.clone(),
                count,
            });
        }
        ResolvedAction::NukePositions => {
            let positions: Vec<(String, String, String)> = state
                .positions
                .iter()
                .filter_map(|pos| {
                    let sym = pos.get("symbol").and_then(|v| v.as_str())?.to_string();
                    let qty_str = pos
                        .get("netQuantity")
                        .or_else(|| pos.get("net_quantity"))
                        .and_then(|v| v.as_str())
                        .unwrap_or("0");
                    let qty: f64 = qty_str.parse().ok()?;
                    if qty == 0.0 {
                        return None;
                    }
                    let side = if qty >= 0.0 { "LONG" } else { "SHORT" };
                    Some((sym, side.to_string(), format!("{:.8}", qty.abs())))
                })
                .collect();
            if positions.is_empty() {
                return Err("No open positions".to_string());
            }
            state.input_mode = InputMode::ConfirmAction(PendingAction::NukePositions { positions });
        }
        ResolvedAction::NukeAll => {
            let order_count = state.open_orders.len();
            let positions: Vec<(String, String, String)> = state
                .positions
                .iter()
                .filter_map(|pos| {
                    let sym = pos.get("symbol").and_then(|v| v.as_str())?.to_string();
                    let qty_str = pos
                        .get("netQuantity")
                        .or_else(|| pos.get("net_quantity"))
                        .and_then(|v| v.as_str())
                        .unwrap_or("0");
                    let qty: f64 = qty_str.parse().ok()?;
                    if qty == 0.0 {
                        return None;
                    }
                    let side = if qty >= 0.0 { "LONG" } else { "SHORT" };
                    Some((sym, side.to_string(), format!("{:.8}", qty.abs())))
                })
                .collect();
            if order_count == 0 && positions.is_empty() {
                return Err("Nothing to nuke — no orders or positions".to_string());
            }
            state.input_mode = InputMode::ConfirmAction(PendingAction::NukeAll {
                order_count,
                positions,
            });
        }
    }
    Ok(())
}

/// A built-in TUI command parsed from the command bar input.
enum TuiCommand {
    Quit,
    Help,
    Search,
    Symbol(String),
    Order,
    Cancel,
    Close,
    Interval(String),
    Panel(String),
    Refresh,
    Bind { key: String, action: String },
    Unbind(String),
    UnbindAll,
    BindList,
    Scenario { args: String },
    Risk,
    StrategyList,
    StrategyRun { name: String },
    Stats,
}

/// Parse a command bar input as a built-in TUI command.
/// Returns None if the input doesn't match any built-in, meaning it should
/// fall through to the CLI subprocess path.
fn parse_tui_command(input: &str) -> Option<TuiCommand> {
    let trimmed = input.trim();
    // Strip optional ! prefix for explicit CLI passthrough
    if trimmed.starts_with('!') {
        return None;
    }
    let parts: Vec<&str> = trimmed.splitn(2, ' ').collect();
    let cmd = parts[0].to_lowercase();
    let arg = parts.get(1).map(|s| s.trim().to_string());

    match cmd.as_str() {
        "q" | "quit" | "exit" => Some(TuiCommand::Quit),
        "help" | "h" | "?" => Some(TuiCommand::Help),
        "search" | "s" if arg.is_none() => Some(TuiCommand::Search),
        "symbol" | "sym" | "switch" => arg.map(TuiCommand::Symbol),
        "order" | "o" => Some(TuiCommand::Order),
        "cancel" => Some(TuiCommand::Cancel),
        "close" if arg.is_none() => Some(TuiCommand::Close),
        "interval" | "i" => arg.map(TuiCommand::Interval),
        "panel" | "p" => arg.map(TuiCommand::Panel),
        "refresh" | "r" => Some(TuiCommand::Refresh),
        "bind" => {
            // :bind list  |  :bind <key> <action>
            match arg.as_deref() {
                Some("list") | Some("help") | None => Some(TuiCommand::BindList),
                Some(rest) => {
                    // Split: first token is key, rest is action
                    let bind_parts: Vec<&str> = rest.splitn(2, ' ').collect();
                    if bind_parts.len() == 2 {
                        Some(TuiCommand::Bind {
                            key: bind_parts[0].to_string(),
                            action: bind_parts[1].trim().to_string(),
                        })
                    } else {
                        // Just a key with no action — show help
                        Some(TuiCommand::BindList)
                    }
                }
            }
        }
        "unbind" => match arg.as_deref() {
            Some("all") => Some(TuiCommand::UnbindAll),
            Some(key) => Some(TuiCommand::Unbind(key.to_string())),
            None => Some(TuiCommand::BindList),
        },
        "scenario" => Some(TuiCommand::Scenario {
            args: arg.unwrap_or_default(),
        }),
        "risk" => Some(TuiCommand::Risk),
        "strategy" => match arg.as_deref() {
            Some(rest) if rest.starts_with("run ") => {
                let name = rest.strip_prefix("run ").unwrap().trim().to_string();
                if name.is_empty() {
                    Some(TuiCommand::StrategyList)
                } else {
                    Some(TuiCommand::StrategyRun { name })
                }
            }
            Some("run") => Some(TuiCommand::StrategyList),
            _ => Some(TuiCommand::StrategyList),
        },
        "stats" => Some(TuiCommand::Stats),
        _ => None,
    }
}

/// Filter markets by search query and category (0 = Spot, 1 = Perps).
fn filter_markets(markets: &[(String, String)], query: &str, category: usize) -> Vec<String> {
    let q = query.to_uppercase();
    markets
        .iter()
        .filter(|(symbol, market_type)| {
            let is_perp = market_type.to_uppercase().contains("PERP");
            let cat_match = if category == 1 { is_perp } else { !is_perp };
            cat_match && symbol.to_uppercase().contains(&q)
        })
        .map(|(s, _)| s.clone())
        .collect()
}

/// Get the appropriate time window in seconds for a kline interval.
fn kline_window_secs(interval: &str) -> i64 {
    match interval {
        "1m" => 3600,       // 60 candles = 1 hour
        "5m" => 5 * 3600,   // 60 candles = 5 hours
        "15m" => 15 * 3600, // 60 candles = 15 hours
        "1h" => 60 * 3600,  // 60 candles = 2.5 days
        "4h" => 240 * 3600, // 60 candles = 10 days
        "1d" => 60 * 86400, // 60 candles = 60 days
        _ => 3600,
    }
}
