//! # Bitfinex Response Parser
//!
//! Parsing JSON array-based responses from Bitfinex API v2.
//!
//! ## Array-Based Format
//!
//! Unlike most exchanges, Bitfinex returns arrays instead of objects:
//! - Ticker: `[BID, BID_SIZE, ASK, ASK_SIZE, ...]`
//! - Order: `[ID, GID, CID, SYMBOL, MTS_CREATE, ...]` (32 fields)
//! - Position: `[SYMBOL, STATUS, AMOUNT, BASE_PRICE, ...]` (18 fields)

use serde_json::Value;

use crate::core::types::{
    ExchangeError, ExchangeResult, AccountType,
    Kline, OrderBook, OrderBookLevel, Ticker, Order, Balance, Position, PublicTrade, TradeSide,
    OrderSide, OrderType, OrderStatus, PositionSide, SymbolInfo, UserTrade,
    OrderbookDelta as OrderbookDeltaData,
    FundingRate, MarkPrice, OpenInterest, AggTrade,
};

// Shared wasm-safe wall-clock helper.
use crate::core::utils::now_ms;

/// Parser for Bitfinex API v2 responses
pub struct BitfinexParser;

impl BitfinexParser {
    // ═══════════════════════════════════════════════════════════════════════════
    // HELPERS
    // ═══════════════════════════════════════════════════════════════════════════

    /// Check if response is an error
    pub fn check_error(response: &Value) -> ExchangeResult<()> {
        if let Some(arr) = response.as_array() {
            if !arr.is_empty() {
                if let Some(first) = arr[0].as_str() {
                    if first == "error" {
                        let code = arr.get(1)
                            .and_then(|v| v.as_i64())
                            .unwrap_or(-1);
                        let message = arr.get(2)
                            .and_then(|v| v.as_str())
                            .unwrap_or("Unknown error")
                            .to_string();
                        return Err(ExchangeError::Api {
                            code: code as i32,
                            message,
                        });
                    }
                }
            }
        }
        Ok(())
    }

    /// Parse f64 from Value (handles both int and float)
    fn parse_f64(value: &Value) -> Option<f64> {
        value.as_f64()
            .or_else(|| value.as_i64().map(|i| i as f64))
    }

    /// Get f64 from array at index
    fn get_f64(arr: &[Value], index: usize) -> Option<f64> {
        arr.get(index).and_then(Self::parse_f64)
    }

    /// Require f64 from array at index
    fn require_f64(arr: &[Value], index: usize) -> ExchangeResult<f64> {
        Self::get_f64(arr, index)
            .ok_or_else(|| ExchangeError::Parse(format!("Missing f64 at index {}", index)))
    }

    /// Get string from array at index
    fn get_str(arr: &[Value], index: usize) -> Option<&str> {
        arr.get(index).and_then(|v| v.as_str())
    }

    /// Require string from array at index
    fn require_str(arr: &[Value], index: usize) -> ExchangeResult<&str> {
        Self::get_str(arr, index)
            .ok_or_else(|| ExchangeError::Parse(format!("Missing string at index {}", index)))
    }

    /// Get i64 from array at index
    fn get_i64(arr: &[Value], index: usize) -> Option<i64> {
        arr.get(index).and_then(|v| v.as_i64())
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // MARKET DATA
    // ═══════════════════════════════════════════════════════════════════════════

    /// Parse ticker (trading pair)
    ///
    /// Live format (verified 2026-06-15 via GET /v2/ticker/tBTCUSD — 11 elements):
    /// `[BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_RELATIVE,
    ///   LAST_PRICE, VOLUME, HIGH, LOW, LAST_TRADE_TS]`
    ///
    /// [0] BID  [1] BID_SIZE  [2] ASK  [3] ASK_SIZE  [4] DAILY_CHANGE
    /// [5] DAILY_CHANGE_RELATIVE  [6] LAST_PRICE  [7] VOLUME (base)
    /// [8] HIGH  [9] LOW  [10] LAST_TRADE_TS (ms, NOT quote volume — no quote volume in this endpoint)
    pub fn parse_ticker(response: &Value, _symbol: &str) -> ExchangeResult<Ticker> {
        Self::check_error(response)?;

        let arr = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array for ticker".to_string()))?;

        if arr.len() < 10 {
            return Err(ExchangeError::Parse(format!("Ticker array too short: {} fields", arr.len())));
        }

        Ok(Ticker {
            last_price: Self::get_f64(arr, 6).unwrap_or(0.0),      // [6] LAST_PRICE
            bid_price: Self::get_f64(arr, 0),                       // [0] BID
            bid_qty: Self::get_f64(arr, 1),                         // [1] BID_SIZE
            ask_price: Self::get_f64(arr, 2),                       // [2] ASK
            ask_qty: Self::get_f64(arr, 3),                         // [3] ASK_SIZE
            high_24h: Self::get_f64(arr, 8),                        // [8] HIGH
            low_24h: Self::get_f64(arr, 9),                         // [9] LOW
            volume_24h: Self::get_f64(arr, 7),                      // [7] VOLUME (base units)
            // arr[10] is LAST_TRADE_TS (ms timestamp), not quote volume.
            // No quote volume exists in the /v2/ticker response.
            quote_volume_24h: None,
            price_change_24h: Self::get_f64(arr, 4),                // [4] DAILY_CHANGE
            price_change_percent_24h: Self::get_f64(arr, 5).map(|r| r * 100.0), // [5] DAILY_CHANGE_RELATIVE
            // Bitfinex REST ticker carries no timestamp field. Stamp the
            // response with the local receive time so downstream consumers
            // can age it. Better than emitting 0 (which gets misread as
            // "1970-01-01" everywhere).
            timestamp: now_ms(), ..Default::default()
        })
    }

    /// Parse orderbook
    ///
    /// Format (P0-P4, verified 2026-06-15 via GET /v2/book/tBTCUSD/P0):
    /// `[[PRICE, COUNT, AMOUNT], ...]`
    /// - [0] PRICE   — price level
    /// - [1] COUNT   — number of orders at this price level → `order_count`
    /// - [2] AMOUNT  — positive = bid, negative = ask
    pub fn parse_orderbook(response: &Value) -> ExchangeResult<OrderBook> {
        Self::check_error(response)?;

        let arr = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array for orderbook".to_string()))?;

        let mut bids = Vec::new();
        let mut asks = Vec::new();

        for item in arr {
            if let Some(level) = item.as_array() {
                if level.len() < 3 { continue; }

                let price = Self::get_f64(level, 0).unwrap_or(0.0);      // [0] PRICE
                let count = Self::get_i64(level, 1).unwrap_or(0) as u32; // [1] COUNT → order_count
                let amount = Self::get_f64(level, 2).unwrap_or(0.0);     // [2] AMOUNT

                if amount > 0.0 {
                    // Positive = bid
                    bids.push(OrderBookLevel::with_count(price, amount, count));
                } else if amount < 0.0 {
                    // Negative = ask
                    asks.push(OrderBookLevel::with_count(price, amount.abs(), count));
                }
            }
        }

        Ok(OrderBook {
            timestamp: now_ms(), // Bitfinex orderbook carries no ts — stamp on receive.
            bids,
            asks,
            sequence: None,
            last_update_id: None,
            first_update_id: None,
            prev_update_id: None,
            event_time: None,
            transaction_time: None,
            checksum: None,
            ..Default::default()
        })
    }

    /// Parse klines/candles
    ///
    /// Format: `[[MTS, OPEN, CLOSE, HIGH, LOW, VOLUME], ...]`
    pub fn parse_klines(response: &Value) -> ExchangeResult<Vec<Kline>> {
        Self::check_error(response)?;

        let arr = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array for candles".to_string()))?;

        let mut klines = Vec::with_capacity(arr.len());

        for item in arr {
            if let Some(candle) = item.as_array() {
                if candle.len() < 6 { continue; }

                // Bitfinex format: [MTS, OPEN, CLOSE, HIGH, LOW, VOLUME]
                let open_time = Self::get_i64(candle, 0).unwrap_or(0);  // [0] MTS

                klines.push(Kline {
                    open_time,
                    open: Self::get_f64(candle, 1).unwrap_or(0.0),      // [1] OPEN
                    close: Self::get_f64(candle, 2).unwrap_or(0.0),     // [2] CLOSE
                    high: Self::get_f64(candle, 3).unwrap_or(0.0),      // [3] HIGH
                    low: Self::get_f64(candle, 4).unwrap_or(0.0),       // [4] LOW
                    volume: Self::get_f64(candle, 5).unwrap_or(0.0),    // [5] VOLUME
                    quote_volume: None,
                    close_time: None,
                    trades: None,
                    ..Default::default()
                });
            }
        }

        // Bitfinex returns newest first by default, reverse to oldest first
        klines.reverse();
        Ok(klines)
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // TRADING
    // ═══════════════════════════════════════════════════════════════════════════

    /// Parse order from array (32 fields)
    ///
    /// Format: `[ID, GID, CID, SYMBOL, MTS_CREATE, MTS_UPDATE, AMOUNT, AMOUNT_ORIG, TYPE, ...]`
    pub fn parse_order(data: &[Value]) -> ExchangeResult<Order> {
        if data.len() < 32 {
            return Err(ExchangeError::Parse(format!("Order array too short: {} fields", data.len())));
        }

        let id = Self::get_i64(data, 0)
            .ok_or_else(|| ExchangeError::Parse("Missing order ID".to_string()))?
            .to_string();

        let symbol = Self::require_str(data, 3)?.to_string();        // [3] SYMBOL

        // [6] AMOUNT: positive=buy, negative=sell
        let amount = Self::get_f64(data, 6).unwrap_or(0.0);
        let side = if amount >= 0.0 {
            OrderSide::Buy
        } else {
            OrderSide::Sell
        };

        // [8] TYPE: "EXCHANGE LIMIT", "EXCHANGE MARKET", etc.
        let order_type_str = Self::get_str(data, 8).unwrap_or("");
        let order_type = if order_type_str.contains("MARKET") {
            OrderType::Market
        } else {
            OrderType::Limit { price: 0.0 }
        };

        // [13] ORDER_STATUS: "ACTIVE", "EXECUTED", "CANCELED", etc.
        let status_str = Self::get_str(data, 13).unwrap_or("ACTIVE");
        let status = Self::parse_order_status(status_str);

        let amount_orig = Self::get_f64(data, 7).unwrap_or(0.0).abs();  // [7] AMOUNT_ORIG
        let filled = (amount_orig - amount.abs()).max(0.0);

        Ok(Order {
            id,
            client_order_id: Self::get_i64(data, 2).map(|i| i.to_string()), // [2] CID
            symbol: Some(symbol),
            side,
            order_type,
            status,
            price: Self::get_f64(data, 16),                          // [16] PRICE
            stop_price: None,
            quantity: amount_orig,
            filled_quantity: filled,
            average_price: Self::get_f64(data, 17),                  // [17] PRICE_AVG
            commission: None,
            commission_asset: None,
            created_at: Self::get_i64(data, 4).unwrap_or(0),         // [4] MTS_CREATE
            updated_at: Self::get_i64(data, 5),                      // [5] MTS_UPDATE
            time_in_force: crate::core::TimeInForce::Gtc,
        })
    }

    /// Parse order status from string
    fn parse_order_status(status: &str) -> OrderStatus {
        match status {
            "ACTIVE" => OrderStatus::New,
            "PARTIALLY FILLED" => OrderStatus::PartiallyFilled,
            "EXECUTED" => OrderStatus::Filled,
            "CANCELED" => OrderStatus::Canceled,
            "INSUFFICIENT BALANCE" => OrderStatus::Rejected,
            "INSUFFICIENT MARGIN" => OrderStatus::Rejected,
            _ => OrderStatus::New,
        }
    }

    /// Parse submit order response
    ///
    /// Format: `[[MTS, TYPE, MESSAGE_ID, null, [ORDER_DATA], CODE, STATUS, TEXT]]`
    pub fn parse_submit_order(response: &Value) -> ExchangeResult<Order> {
        Self::check_error(response)?;

        let outer = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array response".to_string()))?;

        if outer.is_empty() {
            return Err(ExchangeError::Parse("Empty response array".to_string()));
        }

        let notification = outer[0].as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected notification array".to_string()))?;

        // [4] contains the order array (32 fields)
        let order_data = notification.get(4)
            .and_then(|v| v.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing order data".to_string()))?;

        Self::parse_order(order_data)
    }

    /// Parse orders array
    pub fn parse_orders(response: &Value) -> ExchangeResult<Vec<Order>> {
        Self::check_error(response)?;

        let arr = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array of orders".to_string()))?;

        let mut orders = Vec::new();

        for item in arr {
            if let Some(order_data) = item.as_array() {
                if let Ok(order) = Self::parse_order(order_data) {
                    orders.push(order);
                }
            }
        }

        Ok(orders)
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // ACCOUNT
    // ═══════════════════════════════════════════════════════════════════════════

    /// Parse wallets
    ///
    /// Format: `[["exchange", "BTC", BALANCE, UNSETTLED, AVAILABLE, ...], ...]`
    pub fn parse_balances(response: &Value) -> ExchangeResult<Vec<Balance>> {
        Self::check_error(response)?;

        let arr = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array of wallets".to_string()))?;

        let mut balances = Vec::new();

        for item in arr {
            if let Some(wallet) = item.as_array() {
                if wallet.len() < 5 { continue; }

                let asset = Self::get_str(wallet, 1).unwrap_or("").to_string();  // [1] CURRENCY
                if asset.is_empty() { continue; }

                let total = Self::get_f64(wallet, 2).unwrap_or(0.0);             // [2] BALANCE
                let available = Self::get_f64(wallet, 4).unwrap_or(0.0);         // [4] AVAILABLE_BALANCE

                balances.push(Balance {
                    asset,
                    free: available,
                    locked: (total - available).max(0.0),
                    total,
                });
            }
        }

        Ok(balances)
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // POSITIONS
    // ═══════════════════════════════════════════════════════════════════════════

    /// Parse positions
    ///
    /// Format: `[[SYMBOL, STATUS, AMOUNT, BASE_PRICE, FUNDING, ...], ...]` (18 fields)
    pub fn parse_positions(response: &Value) -> ExchangeResult<Vec<Position>> {
        Self::check_error(response)?;

        let arr = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array of positions".to_string()))?;

        let mut positions = Vec::new();

        for item in arr {
            if let Some(pos_data) = item.as_array() {
                if let Some(pos) = Self::parse_position_data(pos_data) {
                    positions.push(pos);
                }
            }
        }

        Ok(positions)
    }

    /// Parse single position from array (18 fields)
    fn parse_position_data(data: &[Value]) -> Option<Position> {
        if data.len() < 18 {
            return None;
        }

        let symbol = Self::get_str(data, 0)?.to_string();           // [0] SYMBOL
        let amount = Self::get_f64(data, 2).unwrap_or(0.0);         // [2] AMOUNT

        // Skip closed positions
        if amount.abs() < f64::EPSILON {
            return None;
        }

        let side = if amount > 0.0 {
            PositionSide::Long
        } else {
            PositionSide::Short
        };

        Some(Position {
            symbol,
            side,
            quantity: amount.abs(),
            entry_price: Self::get_f64(data, 3).unwrap_or(0.0),     // [3] BASE_PRICE
            mark_price: None,
            unrealized_pnl: Self::get_f64(data, 6).unwrap_or(0.0),  // [6] PL
            realized_pnl: None,
            leverage: Self::get_f64(data, 9).map(|l| l as u32).unwrap_or(1), // [9] LEVERAGE
            liquidation_price: Self::get_f64(data, 8),              // [8] PRICE_LIQ
            margin: Self::get_f64(data, 15),                        // [15] COLLATERAL
            margin_type: crate::core::MarginType::Cross,
            take_profit: None,
            stop_loss: None,
        })
    }

    /// Parse user trades (fills) from `/auth/r/trades/hist` or `/auth/r/trades/{symbol}/hist`
    ///
    /// Response format (array of arrays):
    /// `[[ID, PAIR, MTS_CREATE, ORDER_ID, EXEC_AMOUNT, EXEC_PRICE, ORDER_TYPE, ORDER_PRICE, MAKER, FEE, FEE_CURRENCY, ...]]`
    ///
    /// - [0]  ID            — trade ID
    /// - [1]  PAIR          — symbol (e.g. "tBTCUSD")
    /// - [2]  MTS_CREATE    — timestamp in milliseconds
    /// - [3]  ORDER_ID      — linked order ID
    /// - [4]  EXEC_AMOUNT   — executed amount (positive=buy, negative=sell)
    /// - [5]  EXEC_PRICE    — execution price
    /// - [8]  MAKER         — 1 if maker, -1 if taker
    /// - [9]  FEE           — fee amount (negative value)
    /// - [10] FEE_CURRENCY  — fee currency
    pub fn parse_user_trades(response: &Value) -> ExchangeResult<Vec<UserTrade>> {
        Self::check_error(response)?;

        let arr = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array of trades".to_string()))?;

        let mut trades = Vec::with_capacity(arr.len());

        for item in arr {
            let trade_arr = match item.as_array() {
                Some(a) if a.len() >= 11 => a,
                _ => continue,
            };

            let id = Self::get_i64(trade_arr, 0)
                .ok_or_else(|| ExchangeError::Parse("Missing trade ID at index 0".to_string()))?
                .to_string();

            let symbol = Self::get_str(trade_arr, 1)
                .unwrap_or("")
                .to_string();

            let timestamp = Self::get_i64(trade_arr, 2).unwrap_or(0);

            let order_id = Self::get_i64(trade_arr, 3)
                .unwrap_or(0)
                .to_string();

            // EXEC_AMOUNT: positive = buy, negative = sell
            let exec_amount = Self::get_f64(trade_arr, 4).unwrap_or(0.0);
            let side = if exec_amount >= 0.0 {
                OrderSide::Buy
            } else {
                OrderSide::Sell
            };
            let quantity = exec_amount.abs();

            let price = Self::get_f64(trade_arr, 5).unwrap_or(0.0);

            // MAKER: 1 = maker, -1 = taker
            let maker_flag = Self::get_i64(trade_arr, 8).unwrap_or(-1);
            let is_maker = maker_flag == 1;

            // FEE is a negative value; take abs
            let commission = Self::get_f64(trade_arr, 9).map(|f| f.abs()).unwrap_or(0.0);

            let commission_asset = Self::get_str(trade_arr, 10)
                .unwrap_or("")
                .to_string();

            trades.push(UserTrade {
                id,
                order_id,
                symbol,
                side,
                price,
                quantity,
                commission,
                commission_asset,
                is_maker,
                timestamp,
            });
        }

        Ok(trades)
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // WEBSOCKET PARSERS
    // ═══════════════════════════════════════════════════════════════════════════

    /// Parse WebSocket ticker
    ///
    /// Format (same 10-element layout as REST /v2/ticker, no trailing timestamp on WS):
    /// `[BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_RELATIVE,
    ///   LAST_PRICE, VOLUME, HIGH, LOW]`
    ///
    /// [0] BID  [1] BID_SIZE  [2] ASK  [3] ASK_SIZE  [4] DAILY_CHANGE
    /// [5] DAILY_CHANGE_RELATIVE  [6] LAST_PRICE  [7] VOLUME  [8] HIGH  [9] LOW
    pub fn parse_ws_ticker(data: &[Value]) -> ExchangeResult<Ticker> {
        if data.len() < 10 {
            return Err(ExchangeError::Parse(format!("WS Ticker array too short: {} fields", data.len())));
        }

        Ok(Ticker {
            last_price: Self::require_f64(data, 6)?,
            bid_price: Some(Self::require_f64(data, 0)?),
            bid_qty: Self::get_f64(data, 1),                         // [1] BID_SIZE
            ask_price: Some(Self::require_f64(data, 2)?),
            ask_qty: Self::get_f64(data, 3),                         // [3] ASK_SIZE
            high_24h: Self::get_f64(data, 8),
            low_24h: Self::get_f64(data, 9),
            volume_24h: Self::get_f64(data, 7),
            quote_volume_24h: None,
            price_change_24h: Self::get_f64(data, 4),
            price_change_percent_24h: Self::get_f64(data, 5).map(|r| r * 100.0),
            timestamp: crate::core::timestamp_millis() as i64, ..Default::default()
        })
    }

    /// Parse recent public trades from `GET /v2/trades/{symbol}/hist`.
    ///
    /// Response format: `[[ID, MTS, AMOUNT, PRICE], ...]`
    /// - `ID`     — trade identifier (i64)
    /// - `MTS`    — timestamp in milliseconds
    /// - `AMOUNT` — positive = Buy side, negative = Sell side
    /// - `PRICE`  — execution price
    pub fn parse_recent_trades(response: &Value) -> ExchangeResult<Vec<PublicTrade>> {
        Self::check_error(response)?;

        let arr = response
            .as_array()
            .ok_or_else(|| ExchangeError::Parse("parse_recent_trades: expected array".into()))?;

        let mut out = Vec::with_capacity(arr.len());
        for item in arr {
            let row = match item.as_array() {
                Some(a) if a.len() >= 4 => a,
                _ => continue,
            };

            let id = Self::get_i64(row, 0)
                .map(|v| v.to_string())
                .unwrap_or_default();
            let timestamp = Self::get_i64(row, 1).unwrap_or(0);
            let amount = Self::require_f64(row, 2)?;
            let price = Self::require_f64(row, 3)?;

            let side = if amount >= 0.0 {
                TradeSide::Buy
            } else {
                TradeSide::Sell
            };

            out.push(PublicTrade {
                id,
                price,
                quantity: amount.abs(),
                side,
                timestamp,
                ..Default::default()
            });
        }
        Ok(out)
    }

    /// Parse `GET /v2/trades/{symbol}/hist` for the deep-history windowed
    /// pagination path (`Bitfinex::get_agg_trades`).
    ///
    /// Same wire shape as `parse_recent_trades` (`[[ID, MTS, AMOUNT, PRICE],
    /// ...]`) but wrapped as `AggTrade` with `aggregate_id` set to the
    /// trade's own `MTS` (millisecond timestamp) — see the `HistoryCursor::
    /// TsWindow` contract documented on `BitfinexConnector::get_agg_trades`.
    /// Bitfinex has no server-side trade aggregation, so `first_trade_id ==
    /// last_trade_id == MTS` (an aggregate of exactly one constituent trade).
    pub fn parse_agg_trades(response: &Value) -> ExchangeResult<Vec<AggTrade>> {
        Self::check_error(response)?;

        let arr = response
            .as_array()
            .ok_or_else(|| ExchangeError::Parse("parse_agg_trades: expected array".into()))?;

        let mut out = Vec::with_capacity(arr.len());
        for item in arr {
            let row = match item.as_array() {
                Some(a) if a.len() >= 4 => a,
                _ => continue,
            };

            let timestamp = Self::get_i64(row, 1).unwrap_or(0);
            let amount = Self::require_f64(row, 2)?;
            let price = Self::require_f64(row, 3)?;

            out.push(AggTrade {
                aggregate_id: timestamp,
                price,
                quantity: amount.abs(),
                first_trade_id: timestamp,
                last_trade_id: timestamp,
                is_buy: amount >= 0.0,
                timestamp,
                ..Default::default()
            });
        }
        Ok(out)
    }

    /// Parse WebSocket trade
    /// Format: [ID, MTS, AMOUNT, PRICE]
    pub fn parse_ws_trade(data: &[Value]) -> ExchangeResult<PublicTrade> {
        if data.len() < 4 {
            return Err(ExchangeError::Parse(format!("WS Trade array too short: {} fields", data.len())));
        }

        let amount = Self::require_f64(data, 2)?;
        let side = if amount > 0.0 {
            TradeSide::Buy
        } else {
            TradeSide::Sell
        };

        Ok(PublicTrade {
            id: Self::get_i64(data, 0).map(|id| id.to_string()).unwrap_or_default(),
            price: Self::require_f64(data, 3)?,
            quantity: amount.abs(),
            side,
            timestamp: Self::get_i64(data, 1).unwrap_or(0),
            ..Default::default()
        })
    }

    /// Parse WebSocket orderbook delta into a raw `OrderbookDeltaData` payload.
    /// Format: [[PRICE, COUNT, AMOUNT], ...].
    /// Caller (websocket.rs) wraps it into `StreamEvent::OrderbookDelta { symbol, delta }`
    /// with the symbol extracted from the channel-id → subscription map.
    pub fn parse_ws_orderbook_delta(data: &[Value]) -> ExchangeResult<OrderbookDeltaData> {
        let mut bids = Vec::new();
        let mut asks = Vec::new();

        for entry in data {
            if let Some(level) = entry.as_array() {
                if level.len() >= 3 {
                    let price = Self::require_f64(level, 0)?;
                    let count = Self::get_i64(level, 1).unwrap_or(0);
                    let amount = Self::require_f64(level, 2)?;

                    // Count = 0 means remove this price level
                    if count > 0 {
                        let c = count as u32;
                        if amount > 0.0 {
                            bids.push(OrderBookLevel::with_count(price, amount, c));
                        } else {
                            asks.push(OrderBookLevel::with_count(price, amount.abs(), c));
                        }
                    }
                }
            }
        }

        Ok(OrderbookDeltaData {
            bids,
            asks,
            timestamp: crate::core::timestamp_millis() as i64,
            first_update_id: None,
            last_update_id: None,
            prev_update_id: None,
            event_time: None,
            checksum: None,
        })
    }

    /// Parse WebSocket kline (candle)
    /// Format: [MTS, OPEN, CLOSE, HIGH, LOW, VOLUME]
    pub fn parse_ws_kline(data: &[Value]) -> ExchangeResult<Kline> {
        if data.len() < 6 {
            return Err(ExchangeError::Parse(format!("WS Kline array too short: {} fields", data.len())));
        }

        Ok(Kline {
            open_time: Self::get_i64(data, 0).unwrap_or(0),
            open: Self::require_f64(data, 1)?,
            high: Self::require_f64(data, 3)?,
            low: Self::require_f64(data, 4)?,
            close: Self::require_f64(data, 2)?,
            volume: Self::require_f64(data, 5)?,
            close_time: Some(Self::get_i64(data, 0).unwrap_or(0)), // Same as open_time for Bitfinex
            quote_volume: None,
            trades: None,
            ..Default::default()
        })
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // EXCHANGE INFO
    // ═══════════════════════════════════════════════════════════════════════════

    /// Parse exchange info from Bitfinex v1/symbols_details response.
    ///
    /// Response format (array of objects):
    /// ```json
    /// [{"pair":"btcusd","price_precision":5,"initial_margin":"30.0","minimum_margin":"15.0","maximum_order_size":"2000.0","minimum_order_size":"0.00006","expiration":"NA","margin":true},...]
    /// ```
    pub fn parse_exchange_info(response: &Value, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
        let items = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array response".to_string()))?;

        let mut symbols = Vec::with_capacity(items.len());

        for item in items {
            let pair = match item.get("pair").and_then(|v| v.as_str()) {
                Some(p) => p,
                None => continue,
            };

            // Bitfinex pairs are lowercase: "btcusd" -> base="BTC", quote="USD"
            // Most pairs are 6 chars with 3+3 split, but some longer ones exist
            let (base_asset, quote_asset) = Self::split_pair(pair);
            if base_asset.is_empty() || quote_asset.is_empty() {
                continue;
            }

            let price_precision = item.get("price_precision")
                .and_then(|v| v.as_u64())
                .unwrap_or(8) as u8;

            let min_quantity = item.get("minimum_order_size")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<f64>().ok());

            let max_quantity = item.get("maximum_order_size")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<f64>().ok());

            // Use symbol format that Bitfinex API uses (e.g. "tBTCUSD")
            let symbol = format!("t{}", pair.to_uppercase());

            // RAW: Bitfinex v1/symbols_details has no status field.
            // Use empty string rather than faking "TRADING".
            // The `margin` bool and `expiration` field are carried raw in `extra`.
            let status = String::new();

            symbols.push(SymbolInfo {
                symbol,
                base_asset,
                quote_asset,
                status,
                price_precision,
                quantity_precision: 8,
                min_quantity,
                max_quantity,
                tick_size: None,
                step_size: None,
                min_notional: None,
                account_type,
                // Bitfinex v1/symbols_details carries no instrument_type token.
                // Margin-eligible flag and "expiration" are in `extra`.
                instrument_type: None,
                // RAW passthrough — full native symbol record.
                extra: item.clone(),
            });
        }

        Ok(symbols)
    }

    /// Split a Bitfinex pair like "btcusd" into ("BTC", "USD").
    /// Bitfinex uses 3-char base + quote for most symbols, but longer ones exist.
    fn split_pair(pair: &str) -> (String, String) {
        let pair_upper = pair.to_uppercase();
        // Known quote currencies sorted by length descending to avoid ambiguity
        const KNOWN_QUOTES: &[&str] = &["USDT", "USDC", "TUSD", "BUSD", "CNHT", "XAUT", "USD", "EUR", "GBP", "JPY", "BTC", "ETH"];
        for quote in KNOWN_QUOTES {
            if pair_upper.ends_with(quote) {
                let base = &pair_upper[..pair_upper.len() - quote.len()];
                if !base.is_empty() {
                    return (base.to_string(), quote.to_string());
                }
            }
        }
        // Fallback: 3+rest split
        if pair_upper.len() >= 6 {
            let base = pair_upper[..3].to_string();
            let quote = pair_upper[3..].to_string();
            return (base, quote);
        }
        (String::new(), String::new())
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // DERIVATIVES STATUS — field index reference
    // ═══════════════════════════════════════════════════════════════════════════
    //
    // Two endpoint forms with DIFFERENT index offsets:
    //
    // A) ?keys= form  (GET /v2/status/deriv?keys=tBTCF0:USTF0)
    //    Returns [[KEY, MTS, null, DERIV_PRICE, SPOT_PRICE, ...]].
    //    Verified live 2026-06-15.
    //
    //    Idx  Field
    //    ---  -----
    //     0   KEY                        (string, e.g. "tBTCF0:USTF0")
    //     1   MTS                        (i64 ms timestamp)
    //     2   null
    //     3   DERIV_PRICE                (f64, last derivative trade price)
    //     4   SPOT_PRICE                 (f64, index / spot reference price)
    //     5   null
    //     6   INSURANCE_FUND_BALANCE     (f64)
    //     7   null
    //     8   NEXT_FUNDING_EVT_TIMESTAMP (i64 ms)
    //     9   NEXT_FUNDING_ACCRUED       (f64)
    //    10   NEXT_FUNDING_STEP          (i64)
    //    11   null
    //    12   CURRENT_FUNDING            (f64, current hourly funding rate)
    //    13   null
    //    14   null
    //    15   MARK_PRICE                 (f64)
    //    16   null
    //    17   null
    //    18   OPEN_INTEREST              (f64, in base-currency units)
    //
    // B) /hist form  (GET /v2/status/deriv/{symbol}/hist)
    //    The leading KEY element is DROPPED — every index = ?keys= index − 1.
    //    Verified live 2026-06-15.
    //
    //    Idx  Field
    //    ---  -----
    //     0   MTS                        (i64 ms timestamp)
    //     1   null
    //     2   DERIV_PRICE                (f64)
    //     3   SPOT_PRICE                 (f64)
    //     4   null
    //     5   INSURANCE_FUND_BALANCE     (f64)
    //     6   null
    //     7   NEXT_FUNDING_EVT_TIMESTAMP (i64 ms)
    //     8   NEXT_FUNDING_ACCRUED       (f64)
    //     9   NEXT_FUNDING_STEP          (i64)
    //    10   null
    //    11   CURRENT_FUNDING            (f64)
    //    12   null
    //    13   null
    //    14   MARK_PRICE                 (f64)
    //    15   null
    //    16   null
    //    17   OPEN_INTEREST              (f64)
    //
    // All parsers below consume the /hist form.
    // Source: https://docs.bitfinex.com/reference/rest-public-derivatives-status-history

    /// Parse funding rate history from `GET /v2/status/deriv/{symbol}/hist`.
    ///
    /// Extracts `CURRENT_FUNDING` (index 12) and `MTS` (index 1) from each
    /// event snapshot. The response is an outer array of snapshot arrays.
    pub fn parse_deriv_funding_rate_history(
        response: &Value,
        symbol: &str,
    ) -> ExchangeResult<Vec<FundingRate>> {
        Self::check_error(response)?;

        let outer = response
            .as_array()
            .ok_or_else(|| ExchangeError::Parse(
                "bitfinex deriv status hist: expected outer array".into(),
            ))?;

        let mut out = Vec::with_capacity(outer.len());
        for item in outer {
            let row = match item.as_array() {
                Some(a) => a,
                None => continue,
            };
            // For the `{key}/hist` form the key is in the URL, so element 0 is
            // MTS directly (no leading key field). Indices verified live 2026-06-04.
            // idx 0 = MTS (ms timestamp)
            let timestamp = match Self::get_i64(row, 0) {
                Some(t) => t,
                None => continue,
            };
            // idx 11 = CURRENT_FUNDING
            let rate = match Self::get_f64(row, 11) {
                Some(r) => r,
                None => continue,
            };
            // idx 7 = NEXT_FUNDING_EVT_TIMESTAMP (ms)
            let next_funding_time = Self::get_i64(row, 7);
            // idx 14 = MARK_PRICE (hist form; ?keys= form idx 15 minus the leading key element)
            let mark_price = Self::get_f64(row, 14);
            // idx 3 = SPOT_PRICE / index price (hist form; ?keys= form idx 4)
            let index_price = Self::get_f64(row, 3);

            // idx 8 = NEXT_FUNDING_ACCRUED (hist form; ?keys= idx 9 minus leading key)
            let accrued_funding = Self::get_f64(row, 8);
            // idx 9 = NEXT_FUNDING_STEP (hist form; ?keys= idx 10 minus leading key)
            let funding_step = Self::get_i64(row, 9);

            let _ = symbol; // symbol used by caller for routing; not stored in FundingRate
            out.push(FundingRate {
                rate,
                next_funding_time,
                timestamp,
                mark_price,
                index_price,
                accrued_funding,
                funding_step,
                ..Default::default()
            });
        }
        Ok(out)
    }

    /// Parse open interest history from `GET /v2/status/deriv/{symbol}/hist`.
    ///
    /// Extracts `OPEN_INTEREST` (index 17) and `MTS` (index 0) from each
    /// event snapshot. For `{key}/hist` element 0 is MTS (no leading key).
    /// Indices verified live 2026-06-04.
    pub fn parse_deriv_open_interest_history(
        response: &Value,
    ) -> ExchangeResult<Vec<OpenInterest>> {
        Self::check_error(response)?;

        let outer = response
            .as_array()
            .ok_or_else(|| ExchangeError::Parse(
                "bitfinex deriv status hist: expected outer array".into(),
            ))?;

        let mut out = Vec::with_capacity(outer.len());
        for item in outer {
            let row = match item.as_array() {
                Some(a) => a,
                None => continue,
            };
            let timestamp = match Self::get_i64(row, 0) {
                Some(t) => t,
                None => continue,
            };
            // idx 17 = OPEN_INTEREST (base-currency units)
            let oi = match Self::get_f64(row, 17) {
                Some(v) => v,
                None => continue,
            };
            out.push(OpenInterest {
                open_interest: oi,
                open_interest_value: None,
                timestamp, ..Default::default()
            });
        }
        Ok(out)
    }

    /// Parse insurance-fund history from `GET /v2/status/deriv/{symbol}/hist`.
    ///
    /// Same sparse-array endpoint as funding/mark/OI. The insurance/clamp fund
    /// balance sits at `?keys=` idx6 → hist idx5 (offset −1 for the dropped key).
    /// Live-probed 2026-06-15 (BTCF0:USTF0 idx6 ≈ 66432200).
    pub fn parse_deriv_insurance_fund_history(
        response: &Value,
    ) -> ExchangeResult<Vec<crate::core::types::InsuranceFund>> {
        Self::check_error(response)?;

        let outer = response
            .as_array()
            .ok_or_else(|| ExchangeError::Parse(
                "bitfinex deriv status hist: expected outer array".into(),
            ))?;

        let mut out = Vec::with_capacity(outer.len());
        for item in outer {
            let row = match item.as_array() {
                Some(a) => a,
                None => continue,
            };
            let timestamp = match Self::get_i64(row, 0) {
                Some(t) => t,
                None => continue,
            };
            // idx 5 = INSURANCE_FUND_BALANCE (hist form; ?keys= idx6 minus the leading key)
            let balance = match Self::get_f64(row, 5) {
                Some(v) => v,
                None => continue,
            };
            out.push(crate::core::types::InsuranceFund { balance, timestamp });
        }
        Ok(out)
    }

    /// Parse mark-price history from `GET /v2/status/deriv/{symbol}/hist`.
    ///
    /// The `/hist` form carries no leading key element — element [0] is MTS.
    /// Index mapping (hist form = `?keys=` form minus the leading key at [0]):
    /// - [0]  MTS                     → `timestamp`
    /// - [3]  SPOT_PRICE (index)      → `index_price`, `spot_price`
    /// - [11] CURRENT_FUNDING         → `funding_rate`
    /// - [14] MARK_PRICE              → `mark_price`
    ///
    /// Indices verified against live `GET /v2/status/deriv?keys=tBTCF0:USTF0` probe
    /// (2026-06-15): key at [0], so hist offsets are exactly `?keys=` indices − 1.
    pub fn parse_deriv_mark_price_history(
        response: &Value,
        symbol: &str,
    ) -> ExchangeResult<Vec<MarkPrice>> {
        Self::check_error(response)?;

        let outer = response
            .as_array()
            .ok_or_else(|| ExchangeError::Parse(
                "bitfinex deriv status hist: expected outer array".into(),
            ))?;

        let mut out = Vec::with_capacity(outer.len());
        for item in outer {
            let row = match item.as_array() {
                Some(a) => a,
                None => continue,
            };
            // idx 0 = MTS (ms timestamp)
            let timestamp = match Self::get_i64(row, 0) {
                Some(t) => t,
                None => continue,
            };
            // idx 14 = MARK_PRICE (hist form; ?keys= idx 15 minus leading key)
            let mark_price_val = match Self::get_f64(row, 14) {
                Some(v) => v,
                None => continue,
            };
            // idx 2 = DERIV_PRICE (last deriv trade price, hist form; ?keys= idx 3 minus leading key)
            let deriv_price = Self::get_f64(row, 2);
            // idx 3 = SPOT_PRICE / index price (hist form; ?keys= idx 4 minus leading key)
            let spot = Self::get_f64(row, 3);
            // idx 11 = CURRENT_FUNDING (hist form; ?keys= idx 12 minus leading key)
            let funding_rate = Self::get_f64(row, 11);

            let _ = symbol; // caller uses symbol for routing; not stored on MarkPrice
            out.push(MarkPrice {
                mark_price: mark_price_val,
                deriv_price,                                         // [2] DERIV_PRICE (last deriv trade)
                index_price: spot,
                funding_rate,
                timestamp,
                spot_price: spot,
                ..Default::default()
            });
        }
        Ok(out)
    }

    /// Parse position-size series from `GET /v2/stats1/pos.size:1m:{sym}:{side}/hist`.
    ///
    /// Each element: `[MTS, VALUE]`. Returns `(timestamp_ms, position_size)` pairs.
    pub fn parse_pos_size_hist(response: &Value) -> ExchangeResult<Vec<(i64, f64)>> {
        Self::check_error(response)?;

        let outer = response
            .as_array()
            .ok_or_else(|| ExchangeError::Parse(
                "bitfinex stats1 pos.size hist: expected array".into(),
            ))?;

        let mut out = Vec::with_capacity(outer.len());
        for item in outer {
            let row = match item.as_array() {
                Some(a) => a,
                None => continue,
            };
            let ts = match Self::get_i64(row, 0) {
                Some(t) => t,
                None => continue,
            };
            let val = match Self::get_f64(row, 1) {
                Some(v) => v,
                None => continue,
            };
            out.push((ts, val));
        }
        Ok(out)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn test_parse_ticker() {
        let response = json!([
            10645.0,      // BID
            73.93854271,  // BID_SIZE
            10647.0,      // ASK
            75.22266119,  // ASK_SIZE
            731.60645389, // DAILY_CHANGE
            0.0738,       // DAILY_CHANGE_RELATIVE
            10644.00645389, // LAST_PRICE
            14480.89849423, // VOLUME
            10766.0,      // HIGH
            9889.1449809  // LOW
        ]);

        let ticker = BitfinexParser::parse_ticker(&response, "tBTCUSD").unwrap();

        assert!((ticker.last_price - 10644.00645389).abs() < f64::EPSILON);
        assert!((ticker.bid_price.unwrap() - 10645.0).abs() < f64::EPSILON);
        assert!((ticker.ask_price.unwrap() - 10647.0).abs() < f64::EPSILON);
        assert!(ticker.bid_price.unwrap() < ticker.ask_price.unwrap());
    }

    #[test]
    fn test_parse_orderbook() {
        let response = json!([
            [8744.9, 2, 0.45603413],    // bid
            [8744.8, 1, -0.25],         // ask (negative)
            [8744.7, 3, 1.5],           // bid
            [8745.0, 1, -0.75]          // ask
        ]);

        let orderbook = BitfinexParser::parse_orderbook(&response).unwrap();

        assert_eq!(orderbook.bids.len(), 2);
        assert_eq!(orderbook.asks.len(), 2);
        assert!((orderbook.bids[0].price - 8744.9).abs() < f64::EPSILON);
        assert!((orderbook.asks[0].size - 0.25).abs() < f64::EPSILON);
    }

    #[test]
    fn test_parse_klines() {
        let response = json!([
            [1678465320000i64, 20097.0, 20114.0, 20125.0, 20094.0, 1.43504645],
            [1678465260000i64, 20100.0, 20097.0, 20105.0, 20090.0, 0.95234123]
        ]);

        let klines = BitfinexParser::parse_klines(&response).unwrap();

        assert_eq!(klines.len(), 2);
        // Should be reversed (oldest first)
        assert_eq!(klines[0].open_time, 1678465260000i64);
        assert!((klines[0].open - 20100.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_parse_recent_trades() {
        // Live payload (probed 2026-06-14): [ID, MTS, AMOUNT, PRICE]
        // Negative AMOUNT → Sell; positive AMOUNT → Buy
        let response = json!([
            [1936218509i64, 1781450360283i64, -0.0002f64, 63961.0f64],
            [1936218510i64, 1781450360283i64, -0.03339f64, 63960.0f64],
            [1936218511i64, 1781450360284i64, 0.001f64, 63962.0f64]
        ]);

        let trades = BitfinexParser::parse_recent_trades(&response).unwrap();

        assert_eq!(trades.len(), 3);

        // First trade: negative amount → Sell
        assert_eq!(trades[0].id, "1936218509");
        assert_eq!(trades[0].timestamp, 1781450360283i64);
        assert!((trades[0].quantity - 0.0002f64).abs() < f64::EPSILON);
        assert!((trades[0].price - 63961.0f64).abs() < f64::EPSILON);
        assert!(matches!(trades[0].side, TradeSide::Sell));

        // Second trade: negative amount → Sell
        assert!(matches!(trades[1].side, TradeSide::Sell));
        assert!((trades[1].quantity - 0.03339f64).abs() < 1e-10);

        // Third trade: positive amount → Buy
        assert!(matches!(trades[2].side, TradeSide::Buy));
        assert!((trades[2].quantity - 0.001f64).abs() < f64::EPSILON);
    }

    #[test]
    fn test_check_error() {
        let error_response = json!(["error", 10020, "symbol: invalid"]);
        let result = BitfinexParser::check_error(&error_response);

        assert!(result.is_err());
        if let Err(ExchangeError::Api { code, message }) = result {
            assert_eq!(code, 10020);
            assert_eq!(message, "symbol: invalid");
        } else {
            panic!("Expected Api error");
        }
    }
}
