//! # MEXC Parser
//!
//! Parsing MEXC Spot API responses to internal types.
//!
//! ## Response Structure
//!
//! Spot API - Direct response (no wrapper):
//! ```json
//! {
//!   "symbol": "BTCUSDT",
//!   "price": "93200.50"
//! }
//! ```
//!
//! Or array responses:
//! ```json
//! [{ "symbol": "BTCUSDT", "price": "93200.50" }]
//! ```
//!
//! Error response:
//! ```json
//! {
//!   "code": 10001,
//!   "msg": "Missing required parameter"
//! }
//! ```
//!
//! ## Key Differences from Bybit
//!
//! - No wrapper: Spot responses are direct data (no `retCode`/`result` wrapper)
//! - Error detection: Check for `code` field to detect errors
//! - Kline order: [time, open, high, low, close, volume, close_time, quote_volume]
//! - All timestamps in milliseconds
//! - All numeric values as strings

use serde_json::Value;
use crate::core::types::*;
use crate::core::types::{ExchangeResult, ExchangeError};
use crate::core::websocket::KlineInterval;

pub struct MexcParser;

impl MexcParser {
    // ═══════════════════════════════════════════════════════════════════════════════
    // RESPONSE WRAPPER
    // ═══════════════════════════════════════════════════════════════════════════════

    /// Check if response is an error
    ///
    /// MEXC errors have "code" and "msg" fields
    pub fn check_error(json: &Value) -> ExchangeResult<()> {
        if let Some(code) = json.get("code").and_then(|c| c.as_i64()) {
            if code != 0 && code != 200 {
                let msg = json.get("msg")
                    .and_then(|m| m.as_str())
                    .unwrap_or("Unknown error");
                return Err(ExchangeError::Api {
                    code: code as i32,
                    message: msg.to_string(),
                });
            }
        }
        Ok(())
    }

    // ═══════════════════════════════════════════════════════════════════════════════
    // MARKET DATA PARSERS (REST)
    // ═══════════════════════════════════════════════════════════════════════════════

    /// Parse ticker from REST response
    ///
    /// Endpoint: GET /api/v3/ticker/24hr
    /// Response: { symbol, lastPrice, bidPrice, askPrice, highPrice, lowPrice, volume, ... }
    pub fn parse_ticker(json: &Value) -> ExchangeResult<Ticker> {
        Self::check_error(json)?;

        let last_price = json["lastPrice"].as_str()
            .and_then(|s| s.parse::<f64>().ok())
            .ok_or_else(|| ExchangeError::Parse("Invalid lastPrice".into()))?;

        let bid_price = json["bidPrice"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let ask_price = json["askPrice"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let high_24h = json["highPrice"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let low_24h = json["lowPrice"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let volume_24h = json["volume"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let quote_volume_24h = json["quoteVolume"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let price_change_24h = json["priceChange"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let price_change_percent_24h = json["priceChangePercent"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let timestamp = json["closeTime"].as_i64()
            .or_else(|| json["openTime"].as_i64())
            .unwrap_or(0);

        // Live (2026-06-15): "openPrice":"64467.11" — open price 24h ago.
        let open_price = json["openPrice"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        Ok(Ticker {
            last_price,
            bid_price,
            ask_price,
            high_24h,
            low_24h,
            volume_24h,
            quote_volume_24h,
            price_change_24h,
            price_change_percent_24h,
            timestamp,
            open_price,
            ..Default::default()
        })
    }

    /// Parse orderbook from REST response (Spot)
    ///
    /// Endpoint: GET /api/v3/depth
    /// Response: { lastUpdateId, bids: [[price, qty]], asks: [[price, qty]] }
    pub fn parse_orderbook(json: &Value) -> ExchangeResult<OrderBook> {
        Self::check_error(json)?;

        let bids = json["bids"].as_array()
            .ok_or_else(|| ExchangeError::Parse("Missing bids".into()))?
            .iter()
            .filter_map(|entry| {
                let arr = entry.as_array()?;
                let price = arr.first()?.as_str()?.parse::<f64>().ok()?;
                let size = arr.get(1)?.as_str()?.parse::<f64>().ok()?;
                Some(OrderBookLevel::new(price, size))
            })
            .collect();

        let asks = json["asks"].as_array()
            .ok_or_else(|| ExchangeError::Parse("Missing asks".into()))?
            .iter()
            .filter_map(|entry| {
                let arr = entry.as_array()?;
                let price = arr.first()?.as_str()?.parse::<f64>().ok()?;
                let size = arr.get(1)?.as_str()?.parse::<f64>().ok()?;
                Some(OrderBookLevel::new(price, size))
            })
            .collect();

        let timestamp = crate::core::timestamp_millis() as i64;
        let sequence = json["lastUpdateId"].as_i64().map(|u| u.to_string());

        Ok(OrderBook {
            bids,
            asks,
            timestamp,
            sequence,
            last_update_id: None,
            first_update_id: None,
            prev_update_id: None,
            event_time: None,
            transaction_time: None,
            checksum: None,
            ..Default::default()
        })
    }

    /// Parse orderbook from futures REST response
    ///
    /// Endpoint: GET /api/v1/contract/depth/{symbol}
    /// Live (2026-06-15): `{"asks":[[65521.5,262785,5],...],"bids":...}`
    /// Format: [price, quantity, order_count]  — arr[0]=price, arr[1]=qty, arr[2]=order_count
    pub fn parse_orderbook_futures(json: &Value) -> ExchangeResult<OrderBook> {
        let bids = json["bids"].as_array()
            .ok_or_else(|| ExchangeError::Parse("Missing bids in futures orderbook".into()))?
            .iter()
            .filter_map(|entry| {
                let arr = entry.as_array()?;
                let price = arr.first()?.as_f64()?;
                let size = arr.get(1)?.as_f64()?;
                let order_count = arr.get(2).and_then(|v| v.as_u64()).map(|n| n as u32);
                Some(match order_count {
                    Some(c) => OrderBookLevel::with_count(price, size, c),
                    None => OrderBookLevel::new(price, size),
                })
            })
            .collect();

        let asks = json["asks"].as_array()
            .ok_or_else(|| ExchangeError::Parse("Missing asks in futures orderbook".into()))?
            .iter()
            .filter_map(|entry| {
                let arr = entry.as_array()?;
                let price = arr.first()?.as_f64()?;
                let size = arr.get(1)?.as_f64()?;
                let order_count = arr.get(2).and_then(|v| v.as_u64()).map(|n| n as u32);
                Some(match order_count {
                    Some(c) => OrderBookLevel::with_count(price, size, c),
                    None => OrderBookLevel::new(price, size),
                })
            })
            .collect();

        let timestamp = json.get("timestamp")
            .and_then(|t| t.as_i64())
            .unwrap_or_else(|| crate::core::timestamp_millis() as i64);

        let sequence = json.get("version")
            .and_then(|v| v.as_i64())
            .map(|v| v.to_string());

        Ok(OrderBook {
            bids,
            asks,
            timestamp,
            sequence,
            last_update_id: None,
            first_update_id: None,
            prev_update_id: None,
            event_time: None,
            transaction_time: None,
            checksum: None,
            ..Default::default()
        })
    }

    /// Parse klines from REST response (Spot)
    ///
    /// Endpoint: GET /api/v3/klines
    /// Response: [[time, open, high, low, close, volume, close_time, quote_volume], ...]
    ///
    /// Array order: [open_time, open, high, low, close, volume, close_time, quote_volume]
    pub fn parse_klines(json: &Value) -> ExchangeResult<Vec<Kline>> {
        Self::check_error(json)?;

        let list = json.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected klines array".into()))?;

        let klines = list.iter()
            .filter_map(|entry| {
                let arr = entry.as_array()?;

                // MEXC order: [open_time, open, high, low, close, volume, close_time, quote_volume]
                let open_time = arr.first()?.as_i64()?;
                let open = arr.get(1)?.as_str()?.parse::<f64>().ok()?;
                let high = arr.get(2)?.as_str()?.parse::<f64>().ok()?;
                let low = arr.get(3)?.as_str()?.parse::<f64>().ok()?;
                let close = arr.get(4)?.as_str()?.parse::<f64>().ok()?;
                let volume = arr.get(5)?.as_str()?.parse::<f64>().ok()?;
                let close_time = arr.get(6).and_then(|v| v.as_i64());
                let quote_volume = arr.get(7)
                    .and_then(|v| v.as_str())
                    .and_then(|s| s.parse::<f64>().ok());

                Some(Kline {
                    open_time,
                    open,
                    high,
                    low,
                    close,
                    volume,
                    quote_volume,
                    close_time,
                    trades: None,
                    ..Default::default()
                })
            })
            .collect();

        Ok(klines)
    }

    /// Parse klines from futures REST response
    ///
    /// Endpoint: GET /api/v1/contract/kline/{symbol}
    /// Live (2026-06-15): parallel arrays `{time,open,close,high,low,vol,amount,realOpen,realClose,realHigh,realLow}`
    ///
    /// - `time[i]`: Unix seconds → converted to ms.
    /// - `vol[i]`: base-asset volume (contract units).
    /// - `amount[i]`: notional quote-currency volume → `quote_volume`.
    /// - `realOpen/realClose/realHigh/realLow`: TYPE GAP — these are mark-price OHLC vs
    ///   trade-price OHLC; semantic unclear without official docs. Skipped; pending separate
    ///   MarkPrice OHLC research.
    pub fn parse_klines_futures(json: &Value) -> ExchangeResult<Vec<Kline>> {
        // Futures klines have separate arrays for each field
        let time_arr = json.get("time")
            .and_then(|t| t.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing time array".into()))?;

        let open_arr = json.get("open")
            .and_then(|o| o.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing open array".into()))?;

        let high_arr = json.get("high")
            .and_then(|h| h.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing high array".into()))?;

        let low_arr = json.get("low")
            .and_then(|l| l.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing low array".into()))?;

        let close_arr = json.get("close")
            .and_then(|c| c.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing close array".into()))?;

        let vol_arr = json.get("vol")
            .and_then(|v| v.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing vol array".into()))?;

        // amount[i] = notional quote-currency volume (live-verified 2026-06-15).
        let amount_arr = json.get("amount").and_then(|v| v.as_array()).cloned();

        // Ensure all required arrays have the same length
        let len = time_arr.len();
        if open_arr.len() != len || high_arr.len() != len ||
           low_arr.len() != len || close_arr.len() != len || vol_arr.len() != len {
            return Err(ExchangeError::Parse("Inconsistent kline array lengths".into()));
        }

        let mut klines = Vec::with_capacity(len);
        for i in 0..len {
            let open_time = time_arr[i].as_i64()
                .ok_or_else(|| ExchangeError::Parse(format!("Invalid time at index {}", i)))?
                * 1000; // Convert to milliseconds

            let open = open_arr[i].as_f64()
                .ok_or_else(|| ExchangeError::Parse(format!("Invalid open at index {}", i)))?;

            let high = high_arr[i].as_f64()
                .ok_or_else(|| ExchangeError::Parse(format!("Invalid high at index {}", i)))?;

            let low = low_arr[i].as_f64()
                .ok_or_else(|| ExchangeError::Parse(format!("Invalid low at index {}", i)))?;

            let close = close_arr[i].as_f64()
                .ok_or_else(|| ExchangeError::Parse(format!("Invalid close at index {}", i)))?;

            let volume = vol_arr[i].as_f64()
                .ok_or_else(|| ExchangeError::Parse(format!("Invalid volume at index {}", i)))?;

            // amount[i]: notional (quote-currency) volume; live key = "amount".
            let quote_volume = amount_arr.as_deref()
                .and_then(|arr| arr.get(i))
                .and_then(|v| v.as_f64());

            klines.push(Kline {
                open_time,
                open,
                high,
                low,
                close,
                volume,
                quote_volume,
                close_time: None,
                trades: None,
                ..Default::default()
            });
        }

        Ok(klines)
    }

    /// Parse futures ticker from REST response
    ///
    /// Endpoint: GET /api/v1/contract/ticker
    /// Response: { "success": true, "code": 0, "data": { symbol, lastPrice, bid1, ask1,
    ///             volume24, amount24, holdVol, lower24Price, high24Price, riseFallRate,
    ///             indexPrice, fairPrice, fundingRate, timestamp } }
    ///
    /// Live payload (2026-06-15): numbers (not strings); `lower24Price` (not `low24Price`);
    /// `amount24` = quote volume; `fairPrice` = mark price; `holdVol` = open interest.
    pub fn parse_ticker_futures(json: &Value) -> ExchangeResult<Ticker> {
        let last_price = json["lastPrice"].as_f64()
            .ok_or_else(|| ExchangeError::Parse("Invalid lastPrice".into()))?;

        let bid_price = json["bid1"].as_f64();
        let ask_price = json["ask1"].as_f64();
        let high_24h = json["high24Price"].as_f64();
        // Live payload uses "lower24Price" (not "low24Price").
        let low_24h = json["lower24Price"].as_f64();
        let volume_24h = json["volume24"].as_f64();
        let quote_volume_24h = json["amount24"].as_f64();
        let price_change_percent_24h = json["riseFallRate"].as_f64();
        let mark_price = json["fairPrice"].as_f64();
        let index_price = json["indexPrice"].as_f64();
        let funding_rate = json["fundingRate"].as_f64();
        let open_interest = json["holdVol"].as_f64();
        let timestamp = json["timestamp"].as_i64()
            .unwrap_or_else(|| crate::core::timestamp_millis() as i64);

        Ok(Ticker {
            last_price,
            bid_price,
            ask_price,
            high_24h,
            low_24h,
            volume_24h,
            quote_volume_24h,
            price_change_24h: None,
            price_change_percent_24h,
            timestamp,
            mark_price,
            index_price,
            funding_rate,
            open_interest,
            ..Default::default()
        })
    }

    // ═══════════════════════════════════════════════════════════════════════════════
    // RECENT TRADES + AGG TRADES PARSERS (REST)
    // ═══════════════════════════════════════════════════════════════════════════════

    /// Parse spot recent trades from `GET /api/v3/trades`.
    ///
    /// Live payload (2026-06-14):
    /// ```json
    /// [{"id":null,"price":"64119.99","qty":"0.00059686","quoteQty":"...","time":1781450140187,
    ///   "isBuyerMaker":true,"isBestMatch":true,"tradeType":"ASK"}]
    /// ```
    ///
    /// - `id` is always `null` on MEXC — use counter position as id fallback.
    /// - `isBuyerMaker=true` → aggressor was seller → `TradeSide::Sell`.
    pub fn parse_recent_trades_spot(json: &Value) -> ExchangeResult<Vec<PublicTrade>> {
        let arr = json.as_array()
            .ok_or_else(|| ExchangeError::Parse("parse_recent_trades_spot: expected array".into()))?;

        let mut trades = Vec::with_capacity(arr.len());
        for (i, item) in arr.iter().enumerate() {
            let price = item.get("price")
                .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok()))
                .unwrap_or(0.0);

            let quantity = item.get("qty")
                .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok()))
                .unwrap_or(0.0);

            // isBuyerMaker=true: the buyer is the maker → seller is the aggressor.
            let is_buyer_maker = item.get("isBuyerMaker")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            let side = if is_buyer_maker { TradeSide::Sell } else { TradeSide::Buy };

            // MEXC spot trades return id=null; use array index as stable id within this response.
            let id = item.get("id")
                .and_then(|v| v.as_i64())
                .map(|n| n.to_string())
                .unwrap_or_else(|| i.to_string());

            let timestamp = item.get("time")
                .and_then(|v| v.as_i64())
                .unwrap_or(0);

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

            let is_best_match = item.get("isBestMatch")
                .and_then(|v| v.as_bool());

            let trade_type = item.get("tradeType")
                .and_then(|v| v.as_str())
                .map(|s| s.to_string());

            trades.push(PublicTrade {
                id,
                price,
                quantity,
                side,
                timestamp,
                quote_qty,
                is_buyer_maker: Some(is_buyer_maker),
                is_best_match,
                trade_type,
                ..Default::default()
            });
        }
        Ok(trades)
    }

    /// Parse futures recent trades from `GET /api/v1/contract/deals/{symbol}`.
    ///
    /// Live payload (2026-06-14):
    /// ```json
    /// {"success":true,"data":[{"p":64034,"v":74,"T":2,"O":1,"M":2,"t":1781450496978,"i":"14957792772"}]}
    /// ```
    ///
    /// Field meanings: `p`=price, `v`=qty, `T`=direction (1=Buy, 2=Sell), `t`=timestamp ms, `i`=id.
    pub fn parse_recent_trades_futures(json: &Value) -> ExchangeResult<Vec<PublicTrade>> {
        // Response is wrapped: { "success": true, "data": [...] }
        let arr = json.get("data")
            .and_then(|d| d.as_array())
            .ok_or_else(|| ExchangeError::Parse("parse_recent_trades_futures: missing 'data' array".into()))?;

        let mut trades = Vec::with_capacity(arr.len());
        for item in arr {
            let price = item.get("p")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);

            let quantity = item.get("v")
                .and_then(|v| v.as_f64())
                .unwrap_or(0.0);

            // T: 1=Buy, 2=Sell (direction of trade, not maker/taker).
            let side = match item.get("T").and_then(|v| v.as_i64()).unwrap_or(1) {
                2 => TradeSide::Sell,
                _ => TradeSide::Buy,
            };

            let id = item.get("i")
                .and_then(|v| v.as_str().map(|s| s.to_string())
                    .or_else(|| v.as_i64().map(|n| n.to_string())))
                .unwrap_or_default();

            let timestamp = item.get("t")
                .and_then(|v| v.as_i64())
                .unwrap_or(0);

            // O = open/close position direction code, M = self/maker flag code.
            // Raw venue codes, semantics not normalized (live-probed 2026-06-15).
            let open_close_code = item.get("O").and_then(|v| v.as_i64());
            let trade_flag_code = item.get("M").and_then(|v| v.as_i64());

            trades.push(PublicTrade { id, price, quantity, side, timestamp, open_close_code, trade_flag_code, ..Default::default() });
        }
        Ok(trades)
    }

    /// Parse spot agg trades from `GET /api/v3/aggTrades`.
    ///
    /// Live payload (2026-06-14):
    /// ```json
    /// [{"a":null,"f":null,"l":null,"p":"64119.99","q":"0.00123905","T":1781450144000,"m":true,"M":true}]
    /// ```
    ///
    /// - `a`/`f`/`l` are always `null` on MEXC (no agg metadata); `aggregate_id`/
    ///   `first_trade_id`/`last_trade_id` fall back to the trade's own
    ///   millisecond timestamp (`T`) — see `HistoryCursor::TsWindow` contract
    ///   on `MexcConnector::get_agg_trades` (Wave 2, 2026-07-08).
    /// - `m=true` (buyer is maker) → `is_buy=false` (taker sold).
    pub fn parse_agg_trades_spot(json: &Value) -> ExchangeResult<Vec<AggTrade>> {
        let arr = json.as_array()
            .ok_or_else(|| ExchangeError::Parse("parse_agg_trades_spot: expected array".into()))?;

        let mut trades = Vec::with_capacity(arr.len());
        for item in arr {
            let price = item.get("p")
                .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok()))
                .unwrap_or(0.0);

            let quantity = item.get("q")
                .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok()))
                .unwrap_or(0.0);

            // m=true: buyer is maker → is_buy=false (taker is seller).
            let is_buyer_maker = item.get("m")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);

            let timestamp = item.get("T")
                .and_then(|v| v.as_i64())
                .unwrap_or(0);

            // MEXC a/f/l (agg-id, first-fill-id, last-fill-id) are always
            // null — there is no real per-trade ID on this endpoint.
            // `aggregate_id` is the trade's own millisecond timestamp per
            // the `HistoryCursor::TsWindow` contract documented on
            // `MexcConnector::get_agg_trades` / `backfill::
            // agg_trades_paginated` — NOT a synthesized 0 (which would
            // collapse every trade from every page into a single dedup key).
            trades.push(AggTrade {
                aggregate_id: timestamp,
                price,
                quantity,
                first_trade_id: timestamp,
                last_trade_id: timestamp,
                is_buy: !is_buyer_maker,
                timestamp,
                is_best_match: item.get("M").and_then(|v| v.as_bool()),
                ..Default::default()
            });
        }
        Ok(trades)
    }

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

    /// Parse exchange info (symbol list) from MEXC response
    ///
    /// Endpoint: GET /api/v3/exchangeInfo
    /// Response: { symbols: [{ symbol, baseAsset, quoteAsset, status, baseAssetPrecision,
    ///             quoteAssetPrecision, baseSizePrecision, quoteAmountPrecision, ... }] }
    ///
    /// MEXC uses "1" for active status (not "TRADING" like Binance).
    /// Precision values come directly on the symbol object, not in a filters array.
    pub fn parse_exchange_info(json: &Value, account_type: AccountType) -> ExchangeResult<Vec<crate::core::types::SymbolInfo>> {
        Self::check_error(json)?;

        let symbols_arr = json["symbols"].as_array()
            .ok_or_else(|| ExchangeError::Parse("Missing symbols array".into()))?;

        let symbols = symbols_arr.iter()
            .filter_map(|item| {
                let symbol = item["symbol"].as_str()?.to_string();
                let base_asset = item["baseAsset"].as_str().unwrap_or("").to_string();
                let quote_asset = item["quoteAsset"].as_str().unwrap_or("").to_string();

                // RAW: keep native status verbatim (MEXC uses "1"/"0" strings or integers), no filter
                let status = item["status"].as_str()
                    .map(|s| s.to_string())
                    .unwrap_or_else(|| {
                        item["status"].as_i64()
                            .map(|n| n.to_string())
                            .unwrap_or_default()
                    });

                // MEXC provides precision directly on the symbol object.
                // baseAssetPrecision / quoteAssetPrecision are integer decimal places.
                let price_precision = item["quoteAssetPrecision"].as_u64()
                    .or_else(|| item["quotePrecision"].as_u64())
                    .unwrap_or(8) as u8;

                let quantity_precision = item["baseAssetPrecision"].as_u64()
                    .unwrap_or(8) as u8;

                // baseSizePrecision is a string like "0.01" giving the minimum quantity step.
                let step_size = item["baseSizePrecision"].as_str()
                    .and_then(|s| s.parse::<f64>().ok());

                // quoteAmountPrecision is the minimum notional in quote currency (string).
                let min_notional = item["quoteAmountPrecision"].as_str()
                    .and_then(|s| s.parse::<f64>().ok());

                // min_quantity equals the step size for MEXC (no separate minQty field).
                let min_quantity = step_size;
                // MEXC has maxQuoteAmount (in quote currency) but no direct maxQty field.
                let max_quantity: Option<f64> = None;

                // MEXC Spot exchangeInfo has no explicit tickSize field.
                // Derive tick_size from quoteAssetPrecision: 10^(-precision).
                // Also check pricePrecision (integer) which some MEXC futures endpoints use.
                let tick_size = item["quoteAssetPrecision"].as_u64()
                    .or_else(|| item["quotePrecision"].as_u64())
                    .or_else(|| item["pricePrecision"].as_u64())
                    .map(|p| 10f64.powi(-(p as i32)));

                Some(crate::core::types::SymbolInfo {
                    symbol,
                    base_asset,
                    quote_asset,
                    status,
                    price_precision,
                    quantity_precision,
                    min_quantity,
                    max_quantity,
                    tick_size,
                    step_size,
                    min_notional,
                    account_type,
                    // MEXC exchangeInfo does not expose a contract/instrument-type field
                    instrument_type: None,
                    extra: item.clone(),
                })
            })
            .collect();

        Ok(symbols)
    }

    // ═══════════════════════════════════════════════════════════════════════════════
    // ACCOUNT PARSERS (REST)
    // ═══════════════════════════════════════════════════════════════════════════════

    /// Parse balance from REST response
    ///
    /// Endpoint: GET /api/v3/account
    /// Response: { balances: [{ asset, free, locked }] }
    pub fn parse_balance(json: &Value) -> ExchangeResult<Vec<Balance>> {
        Self::check_error(json)?;

        let balances_arr = json["balances"].as_array()
            .ok_or_else(|| ExchangeError::Parse("Missing balances array".into()))?;

        let balances = balances_arr.iter()
            .filter_map(|balance_data| {
                let asset = balance_data["asset"].as_str()?.to_string();
                let free = balance_data["free"].as_str()?.parse::<f64>().ok()?;
                let locked = balance_data["locked"].as_str()?.parse::<f64>().ok().unwrap_or(0.0);
                let total = free + locked;

                // Skip zero balances
                if total == 0.0 {
                    return None;
                }

                Some(Balance {
                    asset,
                    free,
                    locked,
                    total,
                })
            })
            .collect();

        Ok(balances)
    }

    // ═══════════════════════════════════════════════════════════════════════════════
    // TRADING PARSERS (REST)
    // ═══════════════════════════════════════════════════════════════════════════════

    /// Parse order from REST response
    ///
    /// Endpoint: POST /api/v3/order OR GET /api/v3/order
    /// Response: { orderId, symbol, side, type, price, origQty, executedQty, status, ... }
    pub fn parse_order(json: &Value) -> ExchangeResult<Order> {
        Self::check_error(json)?;

        let id = json["orderId"].as_str()
            .ok_or_else(|| ExchangeError::Parse("Missing orderId".into()))?
            .to_string();

        let symbol = json["symbol"].as_str()
            .ok_or_else(|| ExchangeError::Parse("Missing symbol".into()))?
            .to_string();

        let side = match json["side"].as_str() {
            Some("BUY") => OrderSide::Buy,
            Some("SELL") => OrderSide::Sell,
            _ => return Err(ExchangeError::Parse("Invalid side".into())),
        };

        let order_type = match json["type"].as_str() {
            Some("MARKET") => OrderType::Market,
            Some("LIMIT") | Some("LIMIT_MAKER") => OrderType::Limit { price: 0.0 },
            _ => OrderType::Limit { price: 0.0 }, // default
        };

        let status = Self::parse_order_status(json["status"].as_str().unwrap_or(""));

        let price = json["price"].as_str()
            .and_then(|s| s.parse::<f64>().ok());

        let quantity = json["origQty"].as_str()
            .and_then(|s| s.parse::<f64>().ok())
            .unwrap_or(0.0);

        let filled_quantity = json["executedQty"].as_str()
            .and_then(|s| s.parse::<f64>().ok())
            .unwrap_or(0.0);

        let average_price = if filled_quantity > 0.0 {
            json["cummulativeQuoteQty"].as_str()
                .and_then(|s| s.parse::<f64>().ok())
                .map(|quote_qty| quote_qty / filled_quantity)
        } else {
            None
        };

        let created_at = json["time"].as_i64()
            .or_else(|| json["transactTime"].as_i64())
            .unwrap_or(0);

        let updated_at = json["updateTime"].as_i64();

        Ok(Order {
            id,
            client_order_id: json["clientOrderId"].as_str().map(String::from),
            symbol: Some(symbol),
            side,
            order_type,
            status,
            price,
            stop_price: json["stopPrice"].as_str().and_then(|s| s.parse::<f64>().ok()),
            quantity,
            filled_quantity,
            average_price,
            time_in_force: Self::parse_time_in_force(json["timeInForce"].as_str().unwrap_or("GTC")),
            commission: None,
            commission_asset: None,
            created_at,
            updated_at,
        })
    }

    /// Parse order status from string
    fn parse_order_status(status: &str) -> OrderStatus {
        match status {
            "NEW" => OrderStatus::New,
            "PARTIALLY_FILLED" => OrderStatus::PartiallyFilled,
            "FILLED" => OrderStatus::Filled,
            "CANCELED" => OrderStatus::Canceled,
            "REJECTED" => OrderStatus::Rejected,
            "EXPIRED" => OrderStatus::Expired,
            _ => OrderStatus::New,
        }
    }

    /// Parse time in force from string
    fn parse_time_in_force(tif: &str) -> TimeInForce {
        match tif {
            "GTC" => TimeInForce::Gtc,
            "IOC" => TimeInForce::Ioc,
            "FOK" => TimeInForce::Fok,
            _ => TimeInForce::Gtc,
        }
    }

    /// Parse multiple orders from array response
    ///
    /// Used for GET /api/v3/openOrders, GET /api/v3/allOrders
    pub fn parse_orders(json: &Value) -> ExchangeResult<Vec<Order>> {
        Self::check_error(json)?;

        let list = json.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected orders array".into()))?;

        let orders: Vec<Order> = list.iter()
            .filter_map(|order_json| Self::parse_order(order_json).ok())
            .collect();

        Ok(orders)
    }

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

    /// Parse WebSocket message
    ///
    /// MEXC WebSocket format:
    /// { "channel": "spot@public...", "symbol": "BTCUSDT", "sendtime": 123, "publicdeals": {...} }
    pub fn parse_ws_message(json: &Value) -> ExchangeResult<(String, &Value)> {
        // Check for ping/pong
        if let Some(method) = json.get("method").and_then(|m| m.as_str()) {
            return Ok((method.to_string(), json));
        }

        // Check for error/subscription response (has 'msg' but no 'channel')
        if json.get("channel").is_none() {
            if let Some(msg) = json.get("msg").and_then(|m| m.as_str()) {
                return Err(ExchangeError::Parse(format!("Subscription error: {}", msg)));
            }
            return Err(ExchangeError::Parse("Missing channel field".into()));
        }

        // Regular data message
        let channel = json.get("channel")
            .and_then(|c| c.as_str())
            .expect("Safe: already checked above");

        // Data is in stream-specific fields: publicdeals, publicspotkline, etc.
        // For now, just pass the whole message
        Ok((channel.to_string(), json))
    }

    /// Parse WebSocket ticker update (from book ticker stream)
    pub fn parse_ws_ticker(data: &Value) -> ExchangeResult<Ticker> {
        let timestamp = data.get("sendtime")
            .and_then(|t| t.as_i64())
            .unwrap_or(0);

        // Check for book ticker data
        if let Some(book_ticker) = data.get("publicbookticker") {
            let bid_price = book_ticker.get("bidprice")
                .and_then(|p| p.as_str())
                .and_then(|s| s.parse::<f64>().ok());

            let ask_price = book_ticker.get("askprice")
                .and_then(|p| p.as_str())
                .and_then(|s| s.parse::<f64>().ok());

            // Use mid-price as last price
            let last_price = match (bid_price, ask_price) {
                (Some(bid), Some(ask)) => (bid + ask) / 2.0,
                (Some(bid), None) => bid,
                (None, Some(ask)) => ask,
                _ => return Err(ExchangeError::Parse("Missing bid/ask prices".into())),
            };

            return Ok(Ticker {
                last_price,
                bid_price,
                ask_price,
                high_24h: None,
                low_24h: None,
                volume_24h: None,
                quote_volume_24h: None,
                price_change_24h: None,
                price_change_percent_24h: None,
                timestamp, ..Default::default() 
            });
        }

        // Check for trade data
        if let Some(deals) = data.get("publicdeals") {
            if let Some(deals_list) = deals.get("dealsList").and_then(|d| d.as_array()) {
                if let Some(last_deal) = deals_list.first() {
                    let last_price = last_deal.get("price")
                        .and_then(|p| p.as_str())
                        .and_then(|s| s.parse::<f64>().ok())
                        .ok_or_else(|| ExchangeError::Parse("Invalid price".into()))?;

                    return Ok(Ticker {
                        last_price,
                        bid_price: None, // MEXC publicdeals (trade) stream does not carry top-of-book quotes — subscribe to publicbookticker for bid/ask
                        ask_price: None, // MEXC publicdeals (trade) stream does not carry top-of-book quotes — subscribe to publicbookticker for bid/ask
                        high_24h: None,
                        low_24h: None,
                        volume_24h: None,
                        quote_volume_24h: None,
                        price_change_24h: None,
                        price_change_percent_24h: None,
                        timestamp, ..Default::default() 
                    });
                }
            }
        }

        Err(ExchangeError::Parse("Unknown ticker format".into()))
    }

    // ═══════════════════════════════════════════════════════════════════════════════
    // PROTOBUF DECODERS (WebSocket binary messages)
    // ═══════════════════════════════════════════════════════════════════════════════

    // MEXC WebSocket uses protobuf encoding since Aug 2025.
    // All messages are wrapped in PushDataV3ApiWrapper:
    //   field 1 (string): channel
    //   field 3 (string): symbol
    //   field 6 (varint): sendTime (millis)
    //   field 309 (bytes): PublicMiniTickerV3Api
    //   field 314 (bytes): PublicAggreDealsV3Api
    //   field 306 (bytes): PublicBookTickerV3Api
    //
    // Instead of pulling in a protobuf crate, we decode manually since
    // the schema is small and stable.

    /// Decode a varint from protobuf wire format.
    /// Returns (value, new_position).
    fn decode_varint(data: &[u8], mut pos: usize) -> Option<(u64, usize)> {
        let mut result: u64 = 0;
        let mut shift = 0;
        loop {
            if pos >= data.len() { return None; }
            let b = data[pos];
            pos += 1;
            result |= ((b & 0x7f) as u64) << shift;
            if b & 0x80 == 0 { break; }
            shift += 7;
            if shift >= 64 { return None; }
        }
        Some((result, pos))
    }

    /// Extract a string field from protobuf bytes by field number.
    fn pb_string(data: &[u8], target_field: u32) -> Option<String> {
        let mut pos = 0;
        while pos < data.len() {
            let (tag, new_pos) = Self::decode_varint(data, pos)?;
            pos = new_pos;
            let field_num = (tag >> 3) as u32;
            let wire_type = (tag & 0x07) as u8;

            match wire_type {
                0 => { // varint - skip
                    let (_, new_pos) = Self::decode_varint(data, pos)?;
                    pos = new_pos;
                }
                2 => { // length-delimited
                    let (len, new_pos) = Self::decode_varint(data, pos)?;
                    pos = new_pos;
                    let end = pos + len as usize;
                    if end > data.len() { return None; }
                    if field_num == target_field {
                        return String::from_utf8(data[pos..end].to_vec()).ok();
                    }
                    pos = end;
                }
                1 => { pos += 8; } // 64-bit fixed
                5 => { pos += 4; } // 32-bit fixed
                _ => return None,
            }
        }
        None
    }

    /// Extract a varint field from protobuf bytes by field number.
    fn pb_varint(data: &[u8], target_field: u32) -> Option<u64> {
        let mut pos = 0;
        while pos < data.len() {
            let (tag, new_pos) = Self::decode_varint(data, pos)?;
            pos = new_pos;
            let field_num = (tag >> 3) as u32;
            let wire_type = (tag & 0x07) as u8;

            match wire_type {
                0 => {
                    let (val, new_pos) = Self::decode_varint(data, pos)?;
                    pos = new_pos;
                    if field_num == target_field {
                        return Some(val);
                    }
                }
                2 => {
                    let (len, new_pos) = Self::decode_varint(data, pos)?;
                    pos = new_pos;
                    pos += len as usize;
                    if pos > data.len() { return None; }
                }
                1 => { pos += 8; }
                5 => { pos += 4; }
                _ => return None,
            }
        }
        None
    }

    /// Extract a bytes (sub-message) field from protobuf by field number.
    fn pb_bytes(data: &[u8], target_field: u32) -> Option<&[u8]> {
        let mut pos = 0;
        while pos < data.len() {
            let (tag, new_pos) = Self::decode_varint(data, pos)?;
            pos = new_pos;
            let field_num = (tag >> 3) as u32;
            let wire_type = (tag & 0x07) as u8;

            match wire_type {
                0 => {
                    let (_, new_pos) = Self::decode_varint(data, pos)?;
                    pos = new_pos;
                }
                2 => {
                    let (len, new_pos) = Self::decode_varint(data, pos)?;
                    pos = new_pos;
                    let end = pos + len as usize;
                    if end > data.len() { return None; }
                    if field_num == target_field {
                        return Some(&data[pos..end]);
                    }
                    pos = end;
                }
                1 => { pos += 8; }
                5 => { pos += 4; }
                _ => return None,
            }
        }
        None
    }

    /// Extract ALL repeated bytes fields with a given field number.
    fn pb_repeated_bytes(data: &[u8], target_field: u32) -> Vec<&[u8]> {
        let mut results = Vec::new();
        let mut pos = 0;
        while pos < data.len() {
            let (tag, new_pos) = match Self::decode_varint(data, pos) {
                Some(v) => v,
                None => break,
            };
            pos = new_pos;
            let field_num = (tag >> 3) as u32;
            let wire_type = (tag & 0x07) as u8;

            match wire_type {
                0 => {
                    if let Some((_, new_pos)) = Self::decode_varint(data, pos) {
                        pos = new_pos;
                    } else { break; }
                }
                2 => {
                    let (len, new_pos) = match Self::decode_varint(data, pos) {
                        Some(v) => v,
                        None => break,
                    };
                    pos = new_pos;
                    let end = pos + len as usize;
                    if end > data.len() { break; }
                    if field_num == target_field {
                        results.push(&data[pos..end]);
                    }
                    pos = end;
                }
                1 => { pos += 8; }
                5 => { pos += 4; }
                _ => break,
            }
        }
        results
    }

    /// Parse a protobuf binary WebSocket message into a StreamEvent.
    ///
    /// The outer wrapper is PushDataV3ApiWrapper with:
    ///   field 1: channel, field 3: symbol, field 6: sendTime
    ///   field 309: MiniTicker, field 314: AggreDeals, field 306: BookTicker
    pub fn parse_protobuf_message(data: &[u8]) -> ExchangeResult<(String, StreamEvent)> {
        // Extract wrapper fields
        let channel = Self::pb_string(data, 1)
            .ok_or_else(|| ExchangeError::Parse("Missing channel in protobuf wrapper".into()))?;
        let symbol = Self::pb_string(data, 3)
            .unwrap_or_default();
        let timestamp = Self::pb_varint(data, 6)
            .unwrap_or(0) as i64;

        // Dispatch based on channel content
        if channel.contains("miniTicker") {
            // field 309: PublicMiniTickerV3Api
            let body = Self::pb_bytes(data, 309)
                .ok_or_else(|| ExchangeError::Parse("Missing miniTicker body (field 309)".into()))?;
            let ticker = Self::parse_pb_mini_ticker(body, &symbol, timestamp)?;
            Ok((channel, StreamEvent::Ticker { symbol, ticker }))
        } else if channel.contains("aggre.deals") || channel.contains("public.deals") {
            // field 314: PublicAggreDealsV3Api (aggre deals)
            // field 301: PublicDealsV3Api (non-aggre raw deals)
            let body = Self::pb_bytes(data, 314)
                .or_else(|| Self::pb_bytes(data, 301))
                .ok_or_else(|| ExchangeError::Parse("Missing deals body".into()))?;
            let event = Self::parse_pb_aggre_deals(body, &symbol, timestamp)?;
            Ok((channel, event))
        } else if channel.contains("bookTicker") {
            // field 305: PublicBookTickerV3Api (PushDataV3ApiWrapper.publicBookTicker)
            let body = Self::pb_bytes(data, 305)
                .ok_or_else(|| ExchangeError::Parse("Missing bookTicker body (field 305)".into()))?;
            let ticker = Self::parse_pb_book_ticker(body, &symbol, timestamp)?;
            Ok((channel, StreamEvent::Ticker { symbol, ticker }))
        } else if channel.contains("aggre.depth") || channel.contains("public.depth") {
            // field 313: PublicAggreDepthV3Api (aggregated incremental depth updates)
            // Try field 313 first (aggre depth), fall back to nearby fields
            let body = Self::pb_bytes(data, 313)
                .or_else(|| Self::pb_bytes(data, 310))
                .or_else(|| Self::pb_bytes(data, 315))
                .ok_or_else(|| ExchangeError::Parse("Missing aggre.depth body (field 313)".into()))?;
            let orderbook = Self::parse_pb_aggre_depth(body, &symbol, timestamp)?;
            let delta = crate::core::types::OrderbookDelta {
                bids: orderbook.bids,
                asks: orderbook.asks,
                timestamp: orderbook.timestamp,
                first_update_id: orderbook.first_update_id,
                last_update_id: orderbook.last_update_id,
                prev_update_id: orderbook.prev_update_id,
                event_time: orderbook.event_time,
                checksum: orderbook.checksum,
            };
            Ok((channel, StreamEvent::OrderbookDelta { symbol, delta }))
        } else if channel.contains("limit.depth") {
            // field 303: PublicLimitDepthV3Api (N-level snapshot).
            // Same body layout as aggre.depth: field1=repeated bids, field2=repeated asks.
            let body = Self::pb_bytes(data, 303)
                .ok_or_else(|| ExchangeError::Parse("Missing limit.depth body (field 303)".into()))?;
            let orderbook = Self::parse_pb_aggre_depth(body, &symbol, timestamp)?;
            let delta = crate::core::types::OrderbookDelta {
                bids: orderbook.bids,
                asks: orderbook.asks,
                timestamp: orderbook.timestamp,
                first_update_id: orderbook.first_update_id,
                last_update_id: orderbook.last_update_id,
                prev_update_id: orderbook.prev_update_id,
                event_time: orderbook.event_time,
                checksum: orderbook.checksum,
            };
            Ok((channel, StreamEvent::OrderbookDelta { symbol, delta }))
        } else if channel.contains("kline") {
            // field 308: PublicKlineV3Api
            // body: f1=interval(str), f2=openTime(sec varint), f3=open, f4=high, f5=low,
            //       f6=close, f7=vol, f8=quoteVol, f9=closeTime(sec varint)
            // Outer sendTime is at field 5 for kline frames (field 6 for ticker).
            let send_time = Self::pb_varint(data, 5)
                .or_else(|| Self::pb_varint(data, 6))
                .unwrap_or(0) as i64;
            let body = Self::pb_bytes(data, 308)
                .ok_or_else(|| ExchangeError::Parse("Missing kline body (field 308)".into()))?;
            let event = Self::parse_pb_kline(body, send_time, &symbol)?;
            Ok((channel, event))
        } else {
            Err(ExchangeError::Parse(format!("Unsupported protobuf channel: {}", channel)))
        }
    }

    /// Parse PublicKlineV3Api protobuf body.
    ///
    /// body fields: 1=interval(str), 2=openTime(varint sec), 3=open(str), 4=high(str),
    ///              5=low(str), 6=close(str), 7=vol(str), 8=quoteVol(str), 9=closeTime(varint sec)
    fn parse_pb_kline(body: &[u8], _send_time: i64, symbol: &str) -> ExchangeResult<StreamEvent> {
        let open_time = Self::pb_varint(body, 2).unwrap_or(0) as i64 * 1000;

        let open = Self::pb_string(body, 3)
            .and_then(|s| s.parse::<f64>().ok())
            .ok_or_else(|| ExchangeError::Parse("pb_kline: missing open".into()))?;
        let high = Self::pb_string(body, 4)
            .and_then(|s| s.parse::<f64>().ok())
            .ok_or_else(|| ExchangeError::Parse("pb_kline: missing high".into()))?;
        let low = Self::pb_string(body, 5)
            .and_then(|s| s.parse::<f64>().ok())
            .ok_or_else(|| ExchangeError::Parse("pb_kline: missing low".into()))?;
        let close = Self::pb_string(body, 6)
            .and_then(|s| s.parse::<f64>().ok())
            .ok_or_else(|| ExchangeError::Parse("pb_kline: missing close".into()))?;
        let volume = Self::pb_string(body, 7)
            .and_then(|s| s.parse::<f64>().ok())
            .unwrap_or(0.0);
        let quote_volume = Self::pb_string(body, 8)
            .and_then(|s| s.parse::<f64>().ok());
        let close_time = Self::pb_varint(body, 9).map(|t| t as i64 * 1000);

        // interval stored in body field 1 (string)
        let interval = KlineInterval::new(Self::pb_string(body, 1).unwrap_or_default());
        Ok(StreamEvent::Kline {
            symbol: symbol.to_string(),
            interval,
            kline: Kline {
                open_time,
                open,
                high,
                low,
                close,
                volume,
                quote_volume,
                close_time,
                trades: None,
                ..Default::default()
            },
        })
    }

    /// Parse PublicMiniTickerV3Api protobuf body.
    ///
    /// Fields: 1=symbol, 2=lastPrice, 3=priceChange%, 4=?, 5=high, 6=low,
    ///         7=volume, 8=quoteVolume, 9-12=alternate timezone variants
    fn parse_pb_mini_ticker(body: &[u8], _symbol: &str, timestamp: i64) -> ExchangeResult<Ticker> {
        let last_price_str = Self::pb_string(body, 2)
            .ok_or_else(|| ExchangeError::Parse("Missing lastPrice in miniTicker".into()))?;
        let last_price: f64 = last_price_str.parse()
            .map_err(|_| ExchangeError::Parse(format!("Invalid lastPrice: {}", last_price_str)))?;

        let price_change_pct = Self::pb_string(body, 3)
            .and_then(|s| s.parse::<f64>().ok());

        let high_24h = Self::pb_string(body, 5)
            .and_then(|s| s.parse::<f64>().ok());

        let low_24h = Self::pb_string(body, 6)
            .and_then(|s| s.parse::<f64>().ok());

        let volume_24h = Self::pb_string(body, 7)
            .and_then(|s| s.parse::<f64>().ok());

        let quote_volume_24h = Self::pb_string(body, 8)
            .and_then(|s| s.parse::<f64>().ok());

        Ok(Ticker {
            last_price,
            bid_price: None, // MEXC PublicMiniTickerV3Api wire format does not carry top-of-book quotes — use PublicBookTickerV3Api (field 306)
            ask_price: None, // MEXC PublicMiniTickerV3Api wire format does not carry top-of-book quotes — use PublicBookTickerV3Api (field 306)
            high_24h,
            low_24h,
            volume_24h,
            quote_volume_24h,
            price_change_24h: None,
            price_change_percent_24h: price_change_pct,
            timestamp, ..Default::default() 
        })
    }

    /// Parse PublicAggreDealsV3Api protobuf body into a `PublicTrade`.
    ///
    /// Fields: 1 (repeated)=deal items, 2=eventType
    /// Each deal item: 1=price(string), 2=quantity(string), 3=tradeType(varint 1=buy 2=sell), 4=time(varint ms)
    ///
    /// Returns the most recent (last) deal as a `StreamEvent::Trade`.
    pub fn parse_pb_aggre_deals(body: &[u8], symbol: &str, timestamp: i64) -> ExchangeResult<StreamEvent> {
        let deals = Self::pb_repeated_bytes(body, 1);

        if deals.is_empty() {
            return Err(ExchangeError::Parse("No deals in aggre deals message".into()));
        }

        // Use the last (most recent) deal
        let last_deal = deals.last().expect("non-empty checked above");
        let price_str = Self::pb_string(last_deal, 1)
            .ok_or_else(|| ExchangeError::Parse("Missing price in aggre deal".into()))?;
        let price: f64 = price_str.parse()
            .map_err(|_| ExchangeError::Parse(format!("Invalid aggre deal price: {}", price_str)))?;

        let quantity = Self::pb_string(last_deal, 2)
            .and_then(|s| s.parse::<f64>().ok())
            .unwrap_or(0.0);

        // tradeType: 1=buy, 2=sell
        let side = match Self::pb_varint(last_deal, 3).unwrap_or(1) {
            2 => TradeSide::Sell,
            _ => TradeSide::Buy,
        };

        let deal_time = Self::pb_varint(last_deal, 4)
            .map(|t| t as i64)
            .unwrap_or(timestamp);

        Ok(StreamEvent::Trade {
            symbol: symbol.to_string(),
            trade: PublicTrade {
                id: String::new(),
                price,
                quantity,
                side,
                timestamp: deal_time,
                ..Default::default()
            },
        })
    }

    /// Parse PublicBookTickerV3Api protobuf body.
    ///
    /// Fields: 1=bidPrice, 2=bidQuantity, 3=askPrice, 4=askQuantity
    fn parse_pb_book_ticker(body: &[u8], _symbol: &str, timestamp: i64) -> ExchangeResult<Ticker> {
        let bid_price = Self::pb_string(body, 1)
            .and_then(|s| s.parse::<f64>().ok());
        let ask_price = Self::pb_string(body, 3)
            .and_then(|s| s.parse::<f64>().ok());

        let last_price = match (bid_price, ask_price) {
            (Some(b), Some(a)) => (b + a) / 2.0,
            (Some(b), None) => b,
            (None, Some(a)) => a,
            _ => return Err(ExchangeError::Parse("Missing bid/ask in bookTicker".into())),
        };

        Ok(Ticker {
            last_price,
            bid_price,
            ask_price,
            high_24h: None,
            low_24h: None,
            volume_24h: None,
            quote_volume_24h: None,
            price_change_24h: None,
            price_change_percent_24h: None,
            timestamp, ..Default::default() 
        })
    }

    /// Parse PublicAggreDepthV3Api protobuf body into an `OrderBook`.
    ///
    /// MEXC aggre.depth sends incremental delta snapshots via protobuf.
    /// The body layout (based on MEXC's websocket-proto schema) is:
    ///   field 1 (repeated bytes): bid price levels — each sub-message:
    ///     field 1 (string): price, field 2 (string): quantity
    ///   field 2 (repeated bytes): ask price levels — same sub-message layout
    ///   field 3 (varint): sequence / update id
    ///
    /// Zero-quantity levels indicate removals (standard L2 delta convention).
    fn parse_pb_aggre_depth(body: &[u8], symbol: &str, timestamp: i64) -> ExchangeResult<OrderBook> {
        let _ = symbol; // symbol comes from the outer wrapper; not repeated in body

        let bid_entries = Self::pb_repeated_bytes(body, 1);
        let ask_entries = Self::pb_repeated_bytes(body, 2);
        let seq = Self::pb_varint(body, 3);

        let parse_levels = |entries: Vec<&[u8]>| -> Vec<OrderBookLevel> {
            entries.iter().filter_map(|entry| {
                let price_str = Self::pb_string(entry, 1)?;
                let qty_str   = Self::pb_string(entry, 2)?;
                let price: f64 = price_str.parse().ok()?;
                let qty: f64   = qty_str.parse().ok()?;
                Some(OrderBookLevel::new(price, qty))
            }).collect()
        };

        let bids = parse_levels(bid_entries);
        let asks = parse_levels(ask_entries);

        Ok(OrderBook {
            bids,
            asks,
            timestamp,
            sequence: seq.map(|s| s.to_string()),
            last_update_id: seq,
            first_update_id: None,
            prev_update_id: None,
            event_time: Some(timestamp),
            transaction_time: None,
            checksum: None,
            ..Default::default()
        })
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // USER TRADES (FILLS)
    // ═══════════════════════════════════════════════════════════════════════════

    /// Parse user trade fills from MEXC `/api/v3/myTrades`.
    ///
    /// Response is a direct JSON array (no wrapper):
    /// ```json
    /// [{"id":"123","orderId":"456","symbol":"BTCUSDT","side":"BUY","price":"50000",
    ///   "qty":"0.001","commission":"0.01","commissionAsset":"USDT","isMaker":false,"time":1672531200000}]
    /// ```
    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 user trades from MEXC".to_string()))?;

        arr.iter()
            .map(|item| {
                let id = item.get("id")
                    .and_then(|v| v.as_str().map(|s| s.to_string())
                        .or_else(|| v.as_i64().map(|n| n.to_string())))
                    .ok_or_else(|| ExchangeError::Parse("Missing 'id' in trade".to_string()))?;

                let order_id = item.get("orderId")
                    .and_then(|v| v.as_str().map(|s| s.to_string())
                        .or_else(|| v.as_i64().map(|n| n.to_string())))
                    .unwrap_or_default();

                let symbol = item.get("symbol")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let side = match item.get("side")
                    .and_then(|v| v.as_str())
                    .unwrap_or("BUY")
                    .to_uppercase()
                    .as_str()
                {
                    "SELL" => OrderSide::Sell,
                    _ => OrderSide::Buy,
                };

                let price = item.get("price")
                    .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok())
                        .or_else(|| v.as_f64()))
                    .ok_or_else(|| ExchangeError::Parse("Missing 'price' in trade".to_string()))?;

                let quantity = item.get("qty")
                    .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok())
                        .or_else(|| v.as_f64()))
                    .ok_or_else(|| ExchangeError::Parse("Missing 'qty' in trade".to_string()))?;

                let commission = item.get("commission")
                    .and_then(|v| v.as_str().and_then(|s| s.parse::<f64>().ok())
                        .or_else(|| v.as_f64()))
                    .unwrap_or(0.0)
                    .abs();

                let commission_asset = item.get("commissionAsset")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let is_maker = item.get("isMaker")
                    .and_then(|v| v.as_bool())
                    .unwrap_or(false);

                let timestamp = item.get("time")
                    .and_then(|v| v.as_i64())
                    .unwrap_or(0);

                Ok(UserTrade {
                    id,
                    order_id,
                    symbol,
                    side,
                    price,
                    quantity,
                    commission,
                    commission_asset,
                    is_maker,
                    timestamp,
                })
            })
            .collect()
    }
    // ═══════════════════════════════════════════════════════════════════════════════
    // DERIVED KLINE PARSERS (contract API — fair_price / index_price klines)
    // ═══════════════════════════════════════════════════════════════════════════════

    /// Parse MEXC contract fair-price (mark price) or index-price klines.
    ///
    /// Endpoint responses for `/api/v1/contract/kline/fair_price/{symbol}` and
    /// `/api/v1/contract/kline/index_price/{symbol}` share the same structure:
    ///
    /// ```json
    /// {
    ///   "success": true,
    ///   "code": 0,
    ///   "data": {
    ///     "time":   [unix_sec, ...],
    ///     "open":   [f64, ...],
    ///     "close":  [f64, ...],
    ///     "high":   [f64, ...],
    ///     "low":    [f64, ...],
    ///     "amount": [0.0, ...],
    ///     "vol":    [0.0, ...]
    ///   }
    /// }
    /// ```
    ///
    /// Timestamps from MEXC are Unix **seconds** — we convert to ms by ×1000.
    /// vol/amount are always 0.0 per the API docs.
    pub fn parse_derived_klines_futures(response: &Value) -> ExchangeResult<Vec<Kline>> {
        // MEXC contract API wraps in { "success": true, "code": 0, "data": {...} }
        if let Some(code) = response.get("code").and_then(|c| c.as_i64()) {
            if code != 0 {
                let msg = response.get("message")
                    .or_else(|| response.get("msg"))
                    .and_then(|m| m.as_str())
                    .unwrap_or("unknown error");
                return Err(ExchangeError::Api { code: code as i32, message: msg.to_string() });
            }
        }

        let data = response.get("data")
            .ok_or_else(|| ExchangeError::Parse("MEXC kline: missing 'data' field".into()))?;

        let times = data.get("time").and_then(|v| v.as_array())
            .ok_or_else(|| ExchangeError::Parse("MEXC kline: missing 'data.time' array".into()))?;
        let opens  = data.get("open").and_then(|v| v.as_array()).cloned().unwrap_or_default();
        let closes = data.get("close").and_then(|v| v.as_array()).cloned().unwrap_or_default();
        let highs  = data.get("high").and_then(|v| v.as_array()).cloned().unwrap_or_default();
        let lows   = data.get("low").and_then(|v| v.as_array()).cloned().unwrap_or_default();

        let parse_val = |arr: &[Value], idx: usize| -> f64 {
            arr.get(idx)
                .and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
                .unwrap_or(0.0)
        };

        let mut klines = Vec::with_capacity(times.len());
        for (i, ts_val) in times.iter().enumerate() {
            // MEXC timestamps are Unix seconds → convert to ms
            let ts_sec = ts_val.as_i64().unwrap_or(0);
            klines.push(Kline {
                open_time: ts_sec * 1000,
                open:  parse_val(&opens,  i),
                high:  parse_val(&highs,  i),
                low:   parse_val(&lows,   i),
                close: parse_val(&closes, i),
                volume: 0.0, // vol always 0.0 per MEXC contract API docs
                quote_volume: None,
                close_time: None,
                trades: None,
                ..Default::default()
            });
        }

        Ok(klines)
    }

    /// Parse MEXC contract funding rate history.
    ///
    /// Endpoint: `GET /api/v1/contract/funding_rate/history`
    /// Response:
    /// ```json
    /// {
    ///   "success": true,
    ///   "code": 0,
    ///   "data": {
    ///     "resultList": [
    ///       { "symbol": "BTC_USDT", "fundingRate": 0.0001, "settleTime": 1648800000000 },
    ///       ...
    ///     ],
    ///     "pageSize": 20,
    ///     "totalPage": 5,
    ///     "totalCount": 100
    ///   }
    /// }
    /// ```
    ///
    /// `settleTime` is in Unix **milliseconds**.
    /// Note: no date filter exists on this endpoint — use `page_num`/`page_size` pagination.
    /// Parse current funding rate snapshot from `GET /api/v1/contract/funding_rate/{symbol}`.
    ///
    /// Live payload (2026-06-15):
    /// ```json
    /// {"data":{"symbol":"BTC_USDT","fundingRate":0.00002,"maxFundingRate":0.0018,
    ///          "minFundingRate":-0.0018,"collectCycle":8,"nextSettleTime":1781481600000,
    ///          "timestamp":1781465657239,"idxPrice":63831.6,"fairPrice":63803.5}}
    /// ```
    ///
    /// `fairPrice` (MEXC "fair price") acts as the mark price in this context.
    pub fn parse_funding_rate_futures(response: &Value) -> ExchangeResult<FundingRate> {
        if let Some(code) = response.get("code").and_then(|c| c.as_i64()) {
            if code != 0 {
                let msg = response.get("message")
                    .or_else(|| response.get("msg"))
                    .and_then(|m| m.as_str())
                    .unwrap_or("unknown error");
                return Err(ExchangeError::Api { code: code as i32, message: msg.to_string() });
            }
        }

        let data = response.get("data")
            .ok_or_else(|| ExchangeError::Parse("MEXC funding rate: missing 'data'".into()))?;

        let rate = data.get("fundingRate")
            .and_then(|v| v.as_f64())
            .ok_or_else(|| ExchangeError::Parse("MEXC funding rate: missing 'fundingRate'".into()))?;

        let symbol = data.get("symbol")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        let next_funding_time = data.get("nextSettleTime")
            .and_then(|v| v.as_i64());

        let timestamp = data.get("timestamp")
            .and_then(|v| v.as_i64())
            .unwrap_or(0);

        let max_funding_rate = data.get("maxFundingRate")
            .and_then(|v| v.as_f64());

        let min_funding_rate = data.get("minFundingRate")
            .and_then(|v| v.as_f64());

        // collectCycle is hours (integer), stored as f64 for the field type.
        let funding_interval_hours = data.get("collectCycle")
            .and_then(|v| v.as_f64());

        let index_price = data.get("idxPrice")
            .and_then(|v| v.as_f64());

        // MEXC "fairPrice" = mark price in the funding context.
        let mark_price = data.get("fairPrice")
            .and_then(|v| v.as_f64());

        Ok(FundingRate {
            rate,
            next_funding_time,
            timestamp,
            symbol,
            max_funding_rate,
            min_funding_rate,
            funding_interval_hours,
            index_price,
            mark_price,
            ..Default::default()
        })
    }

    pub fn parse_funding_rate_history_futures(response: &Value) -> ExchangeResult<Vec<FundingRate>> {
        if let Some(code) = response.get("code").and_then(|c| c.as_i64()) {
            if code != 0 {
                let msg = response.get("message")
                    .or_else(|| response.get("msg"))
                    .and_then(|m| m.as_str())
                    .unwrap_or("unknown error");
                return Err(ExchangeError::Api { code: code as i32, message: msg.to_string() });
            }
        }

        let data = response.get("data")
            .ok_or_else(|| ExchangeError::Parse("MEXC funding history: missing 'data'".into()))?;

        let list = data.get("resultList")
            .and_then(|v| v.as_array())
            .ok_or_else(|| ExchangeError::Parse("MEXC funding history: missing 'data.resultList'".into()))?;

        let mut rates = Vec::with_capacity(list.len());
        for item in list {
            let rate = item.get("fundingRate")
                .and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
                .unwrap_or(0.0);
            // settleTime is ms (live-verified 2026-06-15)
            let timestamp = item.get("settleTime")
                .and_then(|v| v.as_i64())
                .unwrap_or(0);
            // symbol: "BTC_USDT" (live key = "symbol")
            let symbol = item.get("symbol")
                .and_then(|v| v.as_str())
                .map(|s| s.to_string());
            // collectCycle: integer hours (live value = 8; live key = "collectCycle")
            let funding_interval_hours = item.get("collectCycle")
                .and_then(|v| v.as_i64())
                .map(|h| h as f64);
            rates.push(FundingRate {
                rate,
                next_funding_time: None,
                timestamp,
                symbol,
                funding_interval_hours,
                ..Default::default()
            });
        }

        Ok(rates)
    }
}

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

    #[test]
    fn test_parse_ticker() {
        let json = json!({
            "symbol": "BTCUSDT",
            "lastPrice": "93200.50",
            "bidPrice": "93200.00",
            "askPrice": "93210.00",
            "highPrice": "93500.00",
            "lowPrice": "91800.00",
            "volume": "12345.67",
            "quoteVolume": "1147893256.23",
            "priceChange": "1200.50",
            "priceChangePercent": "1.3",
            "openTime": 1640080800000_i64,
            "closeTime": 1640167200000_i64
        });

        let ticker = MexcParser::parse_ticker(&json).unwrap();
        assert_eq!(ticker.last_price, 93200.50);
        assert_eq!(ticker.bid_price, Some(93200.00));
        assert_eq!(ticker.ask_price, Some(93210.00));
    }

    #[test]
    fn test_parse_orderbook() {
        let json = json!({
            "lastUpdateId": 123456789_i64,
            "bids": [
                ["93220.00", "0.5"],
                ["93210.00", "1.2"]
            ],
            "asks": [
                ["93230.00", "0.8"],
                ["93240.00", "2.1"]
            ]
        });

        let orderbook = MexcParser::parse_orderbook(&json).unwrap();
        assert_eq!(orderbook.bids.len(), 2);
        assert_eq!(orderbook.asks.len(), 2);
        assert_eq!(orderbook.bids[0].price, 93220.00);
        assert_eq!(orderbook.bids[0].size, 0.5);
    }

    #[test]
    fn test_parse_error() {
        let json = json!({
            "code": 10001,
            "msg": "Missing required parameter"
        });

        let result = MexcParser::check_error(&json);
        assert!(result.is_err());

        match result {
            Err(ExchangeError::Api { code, message }) => {
                assert_eq!(code, 10001);
                assert_eq!(message, "Missing required parameter");
            },
            _ => panic!("Expected API error"),
        }
    }

    /// Spot trades: id=null must fall back to array index; isBuyerMaker=true → Sell.
    #[test]
    fn test_parse_recent_trades_spot_null_id_and_side() {
        let json = json!([
            {
                "id": null,
                "price": "64119.99",
                "qty": "0.00059686",
                "quoteQty": "38.28",
                "time": 1781450140187_i64,
                "isBuyerMaker": true,
                "isBestMatch": true,
                "tradeType": "ASK"
            },
            {
                "id": null,
                "price": "64120.00",
                "qty": "0.001",
                "quoteQty": "64.12",
                "time": 1781450140200_i64,
                "isBuyerMaker": false,
                "isBestMatch": true,
                "tradeType": "BID"
            }
        ]);

        let trades = MexcParser::parse_recent_trades_spot(&json).unwrap();
        assert_eq!(trades.len(), 2);

        // First trade: isBuyerMaker=true → Sell; id=null → fallback "0"
        assert_eq!(trades[0].id, "0");
        assert_eq!(trades[0].price, 64119.99);
        assert_eq!(trades[0].quantity, 0.00059686);
        assert!(matches!(trades[0].side, TradeSide::Sell));
        assert_eq!(trades[0].timestamp, 1781450140187);

        // Second trade: isBuyerMaker=false → Buy; id=null → fallback "1"
        assert_eq!(trades[1].id, "1");
        assert!(matches!(trades[1].side, TradeSide::Buy));
    }

    /// Futures deals: T=2 must decode as Sell; i (string id) is preserved; wrapped in "data".
    #[test]
    fn test_parse_recent_trades_futures_t_direction() {
        let json = json!({
            "success": true,
            "data": [
                { "p": 64034, "v": 74, "T": 2, "O": 1, "M": 2, "t": 1781450496978_i64, "i": "14957792772" },
                { "p": 63900, "v": 10, "T": 1, "O": 0, "M": 1, "t": 1781450497000_i64, "i": "14957792773" }
            ]
        });

        let trades = MexcParser::parse_recent_trades_futures(&json).unwrap();
        assert_eq!(trades.len(), 2);

        // T=2 → Sell
        assert_eq!(trades[0].price, 64034.0);
        assert_eq!(trades[0].quantity, 74.0);
        assert!(matches!(trades[0].side, TradeSide::Sell));
        assert_eq!(trades[0].id, "14957792772");
        assert_eq!(trades[0].timestamp, 1781450496978);

        // T=1 → Buy
        assert!(matches!(trades[1].side, TradeSide::Buy));
        assert_eq!(trades[1].id, "14957792773");
    }

    /// Spot aggTrades: a=null → aggregate_id falls back to the trade's own
    /// millisecond timestamp (T) per the HistoryCursor::TsWindow contract
    /// (Wave 2, 2026-07-08) — NOT a synthesized 0, which would collapse
    /// every trade from every page into a single dedup key in
    /// `backfill::agg_trades_paginated`. m=true → is_buy=false.
    #[test]
    fn test_parse_agg_trades_spot_null_id_and_side() {
        let json = json!([
            { "a": null, "f": null, "l": null, "p": "64119.99", "q": "0.00123905", "T": 1781450144000_i64, "m": true, "M": true },
            { "a": null, "f": null, "l": null, "p": "64120.01", "q": "0.002", "T": 1781450145000_i64, "m": false, "M": true }
        ]);

        let trades = MexcParser::parse_agg_trades_spot(&json).unwrap();
        assert_eq!(trades.len(), 2);

        // m=true → buyer is maker → is_buy=false; a=null → aggregate_id=T
        assert_eq!(trades[0].aggregate_id, 1781450144000);
        assert_eq!(trades[0].price, 64119.99);
        assert_eq!(trades[0].quantity, 0.00123905);
        assert!(!trades[0].is_buy);
        assert_eq!(trades[0].timestamp, 1781450144000);
        assert_eq!(trades[0].is_best_match, Some(true));

        // m=false → buyer is taker → is_buy=true
        assert_eq!(trades[1].aggregate_id, 1781450145000);
        assert!(trades[1].is_buy);
    }
}
