//! # Bybit Connector
//!
//! Implementation of all core traits for Bybit V5 API.
//!
//! ## Core Traits
//! - `ExchangeIdentity` - exchange identification
//! - `MarketData` - market data
//! - `Trading` - trading operations
//! - `Account` - account information
//! - `Positions` - futures positions
//!
//! ## Extended Methods
//! Additional Bybit-specific methods as struct methods.

use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use std::time::Duration;

use reqwest::header::HeaderMap;
use serde_json::{json, Value};

use crate::core::{
    HttpClient, Credentials, assemble_rest_url,
    ExchangeId, ExchangeType, AccountType, Symbol,
    ExchangeError, ExchangeResult,
    Price, Kline, Ticker, OrderBook,
    Order, OrderSide, OrderType, Balance, AccountInfo,
    Position, FundingRate,
    OrderRequest, CancelRequest, CancelScope,
    BalanceQuery, PositionQuery, PositionModification,
    OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
    UserTrade, UserTradeFilter,
    MarginType,
    AmendRequest, CancelAllResponse, OrderResult,
    TransferResponse, DepositAddress, WithdrawResponse, FundsRecord,
    SymbolInput,
};
use crate::core::types::{
    TransferRequest, TransferHistoryFilter,
    WithdrawRequest, FundsHistoryFilter, FundsRecordType,
    SubAccountOperation, SubAccountResult,
    OpenInterest, LongShortRatio, MarkPrice,
};
use crate::core::traits::{
    ExchangeIdentity, MarketData, Trading, Account, Positions,
    CancelAll, AmendOrder, BatchOrders,
    AccountTransfers, CustodialFunds, SubAccounts,
    FundingHistory, AccountLedger,
    MarketDataPublic,
};
use crate::core::types::PublicTrade;
use crate::core::types::{
    ConnectorStats,
    FundingPayment, FundingFilter,
    LedgerEntry, LedgerFilter,
    MarketDataCapabilities, TradingCapabilities, AccountCapabilities,
};
use crate::core::utils::{RuntimeLimiter, RateLimitMonitor, RateLimitPressure};
use crate::core::types::{RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, OrderbookCapabilities, WsBookChannel};

use super::endpoints::{BybitUrls, BybitEndpoint, format_symbol, account_type_to_category, account_type_to_transfer_type, map_kline_interval};
use super::auth::BybitAuth;
use super::parser::BybitParser;

// ═══════════════════════════════════════════════════════════════════════════════
// RATE LIMIT CAPABILITIES (static — embedded in binary, no allocation)
// ═══════════════════════════════════════════════════════════════════════════════

static BYBIT_POOLS: &[RestLimitPool] = &[RestLimitPool {
    name: "default",
    max_budget: 600,
    window_seconds: 5,
    is_weight: true,
    has_server_headers: true,
    server_header: Some("X-Bapi-Limit-Status"),
    header_reports_used: false,
}];

static BYBIT_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
    model: LimitModel::Weight,
    rest_pools: BYBIT_POOLS,
    decaying: None,
    endpoint_weights: &[],
    ws: WsLimits {
        max_connections: None,
        max_subs_per_conn: None,
        max_msg_per_sec: None,
        max_streams_per_conn: None,
    },
};

// ═══════════════════════════════════════════════════════════════════════════════
// CONNECTOR
// ═══════════════════════════════════════════════════════════════════════════════

/// Bybit connector
pub struct BybitConnector {
    /// HTTP client
    http: HttpClient,
    /// Authentication (None for public methods)
    auth: Option<BybitAuth>,
    /// Testnet mode
    testnet: bool,
    /// REST base URL override for proxy / Path-B routing.
    /// When set, replaces `BybitUrls::base_url(self.testnet)` in every request.
    rest_override: Option<String>,
    /// Runtime rate limiter (Weight model: 600 weight per 5 seconds)
    limiter: Arc<Mutex<RuntimeLimiter>>,
    /// Pressure monitor — gates non-essential requests at >= 90%
    monitor: Arc<Mutex<RateLimitMonitor>>,
    /// Per-symbol precision cache (populated from get_exchange_info)
    precision: crate::core::utils::precision::PrecisionCache,
}

impl BybitConnector {
    /// Create new connector
    pub async fn new(credentials: Option<Credentials>, testnet: bool) -> ExchangeResult<Self> {
        Self::new_with_override(credentials, testnet, None).await
    }

    /// Create new connector with optional REST base URL override.
    ///
    /// When `rest_override` is `Some(url)`, all REST requests use that URL as
    /// the base instead of the exchange's native endpoint.
    pub async fn new_with_override(credentials: Option<Credentials>, testnet: bool, rest_override: Option<String>) -> ExchangeResult<Self> {
        let http = HttpClient::new(30_000)?; // 30 sec timeout

        let mut auth = credentials.as_ref().map(BybitAuth::new);

        // Sync time with server if we have auth
        if auth.is_some() {
            let base_url = BybitUrls::base_url(testnet);
            let url = format!("{}/v5/market/time", base_url);
            if let Ok(response) = http.get(&url, &HashMap::new()).await {
                if let Some(time_sec) = response.get("result")
                    .and_then(|r| r.get("timeSecond"))
                    .and_then(|t| t.as_str())
                    .and_then(|s| s.parse::<i64>().ok())
                {
                    if let Some(ref mut a) = auth {
                        a.sync_time(time_sec * 1000); // Convert to milliseconds
                    }
                }
            }
        }

        let limiter = Arc::new(Mutex::new(RuntimeLimiter::from_caps(&BYBIT_RATE_CAPS)));
        let monitor = Arc::new(Mutex::new(RateLimitMonitor::new("Bybit")));

        Ok(Self {
            http,
            auth,
            testnet,
            rest_override,
            limiter,
            monitor,
            precision: crate::core::utils::precision::PrecisionCache::new(),
        })
    }

    /// Create connector only for public methods
    pub async fn public(testnet: bool, rest_override: Option<String>) -> ExchangeResult<Self> {
        Self::new_with_override(None, testnet, rest_override).await
    }


    // ═══════════════════════════════════════════════════════════════════════════
    // HTTP HELPERS
    // ═══════════════════════════════════════════════════════════════════════════

    /// Sync limiter from Bybit response headers.
    ///
    /// Bybit reports: X-Bapi-Limit-Status = remaining, X-Bapi-Limit = total limit.
    fn update_rate_from_headers(&self, headers: &HeaderMap) {
        let remaining = headers
            .get("X-Bapi-Limit-Status")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.parse::<u32>().ok());

        let limit = headers
            .get("X-Bapi-Limit")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.parse::<u32>().ok());

        if let (Some(remaining), Some(limit)) = (remaining, limit) {
            let used = limit.saturating_sub(remaining);
            if let Ok(mut limiter) = self.limiter.lock() {
                limiter.update_from_server("default", used);
            }
        }
    }

    /// Wait for rate limit budget. Non-essential requests are dropped at >= 90% utilization.
    ///
    /// Returns `true` if acquired, `false` if dropped due to cutoff pressure.
    /// Trading endpoints should pass `essential: true` to always wait through.
    async fn rate_limit_wait(&self, weight: u32, essential: bool) -> bool {
        loop {
            let wait_time = {
                let mut limiter = self.limiter.lock()
                    .expect("rate limiter mutex poisoned");

                let pressure = self.monitor.lock()
                    .expect("rate monitor mutex poisoned")
                    .check(&mut limiter);
                if pressure >= RateLimitPressure::Cutoff && !essential {
                    return false;
                }

                if limiter.try_acquire("default", weight) {
                    return true;
                }
                limiter.time_until_ready("default", weight)
            };
            if wait_time > Duration::ZERO {
                tokio::time::sleep(wait_time).await;
            }
        }
    }

    /// GET request
    async fn get(
        &self,
        endpoint: BybitEndpoint,
        params: HashMap<String, String>,
    ) -> ExchangeResult<Value> {
        // Market data = non-essential: drop at >= 90% utilization to preserve budget for trading
        if !self.rate_limit_wait(1, false).await {
            return Err(ExchangeError::RateLimitExceeded {
                retry_after: None,
                message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
            });
        }

        let real_base = BybitUrls::base_url(self.testnet);
        let path = endpoint.path();

        // Build query string
        let query = if params.is_empty() {
            String::new()
        } else {
            let qs: Vec<String> = params.iter()
                .map(|(k, v)| format!("{}={}", k, v))
                .collect();
            qs.join("&")
        };

        let query_sfx = if query.is_empty() { String::new() } else { format!("?{}", query) };
        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, path, &query_sfx);

        // Add auth headers if needed
        let headers = if endpoint.is_private() {
            let auth = self.auth.as_ref()
                .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
            auth.sign_request("GET", &query)
        } else {
            HashMap::new()
        };

        let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &headers).await?;
        self.update_rate_from_headers(&resp_headers);
        self.check_response(&response)?;
        Ok(response)
    }

    /// POST request
    async fn post(
        &self,
        endpoint: BybitEndpoint,
        body: Value,
    ) -> ExchangeResult<Value> {
        // Order placement = essential: always wait, never drop
        self.rate_limit_wait(1, true).await;

        let real_base = BybitUrls::base_url(self.testnet);
        let path = endpoint.path();
        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, path, "");

        // Auth headers
        let auth = self.auth.as_ref()
            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
        let body_str = body.to_string();
        let headers = auth.sign_request("POST", &body_str);

        let (response, resp_headers) = self.http.post_with_response_headers(&url, &body, &headers).await?;
        self.update_rate_from_headers(&resp_headers);
        self.check_response(&response)?;
        Ok(response)
    }

    /// Check response for errors
    fn check_response(&self, response: &Value) -> ExchangeResult<()> {
        let ret_code = response.get("retCode")
            .and_then(|c| c.as_i64())
            .unwrap_or(-1);

        if ret_code != 0 {
            let msg = response.get("retMsg")
                .and_then(|m| m.as_str())
                .unwrap_or("Unknown error");
            return Err(ExchangeError::Api {
                code: ret_code as i32,
                message: msg.to_string(),
            });
        }

        Ok(())
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // EXTENDED METHODS (Bybit-specific)
    // ═══════════════════════════════════════════════════════════════════════════

    /// Get all tickers
    pub async fn get_all_tickers(&self, account_type: AccountType) -> ExchangeResult<Vec<Ticker>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());

        let response = self.get(BybitEndpoint::Ticker, params).await?;

        let result = response.get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing 'result' field".to_string()))?;
        let list = result.get("list")
            .and_then(|v| v.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing 'result.list' array".to_string()))?;

        let timestamp = response.get("time").and_then(|t| t.as_i64()).unwrap_or(0);

        let tickers = list.iter().map(|data| {
            // parse non-empty string → f64; empty "" → None
            let parse_ne = |key: &str| -> Option<f64> {
                data.get(key).and_then(|v| v.as_str()).filter(|s| !s.is_empty()).and_then(|s| s.parse().ok())
            };
            let parse_ne_i64 = |key: &str| -> Option<i64> {
                data.get(key).and_then(|v| v.as_str()).filter(|s| !s.is_empty()).and_then(|s| s.parse().ok())
            };

            let last_price = parse_ne("lastPrice").unwrap_or(0.0);
            let prev_price = parse_ne("prevPrice24h");
            let price_change_24h = prev_price.map(|p| last_price - p);
            let price_change_percent_24h = data.get("price24hPcnt")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<f64>().ok())
                .map(|v| v * 100.0);

            Ticker {
                last_price,
                bid_price: parse_ne("bid1Price"),
                ask_price: parse_ne("ask1Price"),
                high_24h: parse_ne("highPrice24h"),
                low_24h: parse_ne("lowPrice24h"),
                volume_24h: parse_ne("volume24h"),
                quote_volume_24h: parse_ne("turnover24h"),
                price_change_24h,
                price_change_percent_24h,
                timestamp,
                // ── Top-of-book sizes ──
                bid_qty: parse_ne("bid1Size"),
                ask_qty: parse_ne("ask1Size"),
                // ── Extra price stats ──
                prev_price_24h: parse_ne("prevPrice24h"),
                prev_price_1h: parse_ne("prevPrice1h"),
                turnover_24h: parse_ne("turnover24h"),
                // ── Derivatives fields ──
                mark_price: parse_ne("markPrice"),
                index_price: parse_ne("indexPrice"),
                open_interest: parse_ne("openInterest"),
                open_interest_value: parse_ne("openInterestValue"),
                single_open_interest: parse_ne("singleOpenInterest"),
                funding_rate: parse_ne("fundingRate"),
                next_funding_time: parse_ne_i64("nextFundingTime"),
                funding_interval_hour: parse_ne("fundingIntervalHour"),
                funding_cap: parse_ne("fundingCap"),
                basis: parse_ne("basis"),
                basis_rate: parse_ne("basisRate").or_else(|| parse_ne("basisRateYear")),
                predicted_delivery_price: parse_ne("predictedDeliveryPrice"),
                delivery_time: data.get("deliveryTime").and_then(|v| v.as_str()).and_then(|s| s.parse::<i64>().ok()),
                ..Default::default()
            }
        }).collect();

        Ok(tickers)
    }

    /// Get symbols
    pub async fn get_symbols(&self, account_type: AccountType) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());

        self.get(BybitEndpoint::Symbols, params).await
    }

    /// Cancel all orders
    pub async fn cancel_all_orders(
        &self,
        symbol: Option<Symbol>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<String>> {
        let mut body = json!({
            "category": account_type_to_category(account_type),
        });

        if let Some(s) = symbol {
            body["symbol"] = json!(format_symbol(&s, account_type));
        }

        let response = self.post(BybitEndpoint::CancelAllOrders, body).await?;

        // Parse cancelled order IDs
        let result = response.get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;

        let ids = result.get("list")
            .and_then(|v| v.as_array())
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v.get("orderId").and_then(|id| id.as_str()).map(String::from))
                    .collect()
            })
            .unwrap_or_default();

        Ok(ids)
    }

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

    /// Get open interest history for a symbol.
    ///
    /// `category`: `"linear"` | `"inverse"`.
    /// `interval_time`: `"5min"` | `"15min"` | `"30min"` | `"1h"` | `"4h"` | `"1d"`.
    pub async fn get_open_interest(
        &self,
        category: &str,
        symbol: &str,
        interval_time: &str,
        limit: Option<u32>,
        start_time: Option<i64>,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<crate::core::types::OpenInterest>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        params.insert("intervalTime".to_string(), interval_time.to_string());
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        if let Some(st) = start_time {
            params.insert("startTime".to_string(), st.to_string());
        }
        if let Some(et) = end_time {
            params.insert("endTime".to_string(), et.to_string());
        }
        let response = self.get(BybitEndpoint::OpenInterest, params).await?;
        BybitParser::parse_open_interest_list(&response)
    }

    /// Get historical funding rates.
    ///
    /// Endpoint: `GET /v5/market/funding/history` — no auth.
    /// `category`: `"linear"` | `"inverse"`. `symbol`: e.g. `"BTCUSDT"`.
    /// `limit`: max 200 (default 200). `start_time` / `end_time`: Unix ms.
    pub async fn get_funding_rate_history(
        &self,
        category: &str,
        symbol: &str,
        start_time: Option<i64>,
        end_time: Option<i64>,
        limit: Option<u32>,
    ) -> ExchangeResult<Vec<FundingRate>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        if let Some(t) = start_time {
            params.insert("startTime".to_string(), t.to_string());
        }
        if let Some(t) = end_time {
            params.insert("endTime".to_string(), t.to_string());
        }
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        let response = self.get(BybitEndpoint::FundingRate, params).await?;
        BybitParser::parse_funding_rates(&response)
    }

    /// Get long/short ratio for a symbol.
    ///
    /// `category`: `"linear"` | `"inverse"`. Bybit `ratio_type` is always `"account"`.
    /// `period`: `"5min"` | `"15min"` | `"30min"` | `"1h"` | `"4h"` | `"1d"`.
    pub async fn get_long_short_ratio(
        &self,
        category: &str,
        symbol: &str,
        period: &str,
        limit: Option<u32>,
    ) -> ExchangeResult<Vec<crate::core::types::LongShortRatio>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        params.insert("period".to_string(), period.to_string());
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        let response = self.get(BybitEndpoint::LongShortRatio, params).await?;
        BybitParser::parse_long_short_ratios(&response, symbol, "account")
    }

    /// Get mark price kline data.
    ///
    /// `category`: `"linear"` | `"inverse"`.
    /// `interval`: standard interval string (e.g. `"1m"`, `"1h"`, `"1d"`).
    pub async fn get_mark_price_kline(
        &self,
        category: &str,
        symbol: &str,
        interval: &str,
        limit: Option<u32>,
        start: Option<i64>,
        end: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        params.insert("interval".to_string(), map_kline_interval(interval).to_string());
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        if let Some(st) = start {
            params.insert("start".to_string(), st.to_string());
        }
        if let Some(et) = end {
            params.insert("end".to_string(), et.to_string());
        }
        let response = self.get(BybitEndpoint::MarkPriceKline, params).await?;
        BybitParser::parse_mark_price_kline(&response)
    }

    /// Get index price kline data.
    ///
    /// `category`: `"linear"` | `"inverse"`.
    /// `interval`: standard interval string (e.g. `"1m"`, `"1h"`, `"1d"`).
    pub async fn get_index_price_kline(
        &self,
        category: &str,
        symbol: &str,
        interval: &str,
        limit: Option<u32>,
        start: Option<i64>,
        end: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        params.insert("interval".to_string(), map_kline_interval(interval).to_string());
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        if let Some(st) = start {
            params.insert("start".to_string(), st.to_string());
        }
        if let Some(et) = end {
            params.insert("end".to_string(), et.to_string());
        }
        let response = self.get(BybitEndpoint::IndexPriceKline, params).await?;
        BybitParser::parse_mark_price_kline(&response)
    }

    /// Get premium index price kline data.
    ///
    /// `category`: `"linear"` | `"inverse"`.
    /// `interval`: standard interval string (e.g. `"1m"`, `"1h"`, `"1d"`).
    pub async fn get_premium_index_kline(
        &self,
        category: &str,
        symbol: &str,
        interval: &str,
        limit: Option<u32>,
        start: Option<i64>,
        end: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        params.insert("interval".to_string(), map_kline_interval(interval).to_string());
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        if let Some(st) = start {
            params.insert("start".to_string(), st.to_string());
        }
        if let Some(et) = end {
            params.insert("end".to_string(), et.to_string());
        }
        let response = self.get(BybitEndpoint::PremiumIndexKline, params).await?;
        BybitParser::parse_mark_price_kline(&response)
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // FILL / TRADE HISTORY
    // ═══════════════════════════════════════════════════════════════════════════

    /// Get personal trade fills (executions).
    ///
    /// `start_time` and `end_time` are Unix milliseconds.
    pub async fn get_my_trades(
        &self,
        symbol: Option<&str>,
        account_type: AccountType,
        limit: Option<u32>,
        start_time: Option<i64>,
        end_time: Option<i64>,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        if let Some(s) = symbol {
            params.insert("symbol".to_string(), s.to_string());
        }
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        if let Some(st) = start_time {
            params.insert("startTime".to_string(), st.to_string());
        }
        if let Some(et) = end_time {
            params.insert("endTime".to_string(), et.to_string());
        }
        self.get(BybitEndpoint::MyTrades, params).await
    }

    /// Get closed PnL history for futures positions.
    ///
    /// `start_time` and `end_time` are Unix milliseconds.
    pub async fn get_closed_pnl(
        &self,
        symbol: Option<&str>,
        account_type: AccountType,
        limit: Option<u32>,
        start_time: Option<i64>,
        end_time: Option<i64>,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        if let Some(s) = symbol {
            params.insert("symbol".to_string(), s.to_string());
        }
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        if let Some(st) = start_time {
            params.insert("startTime".to_string(), st.to_string());
        }
        if let Some(et) = end_time {
            params.insert("endTime".to_string(), et.to_string());
        }
        self.get(BybitEndpoint::ClosedPnl, params).await
    }

    /// Get institutional loan product information.
    ///
    /// Endpoint: `GET /v5/ins-loan/product-infos` — public, no auth required.
    /// Returns the list of loan products with leverage and symbol info.
    pub async fn get_institutional_loan_products(&self) -> ExchangeResult<Value> {
        self.get(BybitEndpoint::InsLoanProducts, HashMap::new()).await
    }

    /// Get risk limit tiers for a symbol.
    ///
    /// Endpoint: `GET /v5/market/risk-limit` — public, no auth required.
    /// `category`: `"linear"` | `"inverse"`.
    /// Returns tier list with `riskLimitValue`, `maintenanceMargin`, `initialMargin`, `maxLeverage`.
    pub async fn get_risk_limit(
        &self,
        category: &str,
        symbol: &str,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        self.get(BybitEndpoint::RiskLimit, params).await
    }

    /// Get futures delivery reference price.
    ///
    /// Endpoint: `GET /v5/market/delivery-price` — public, no auth required.
    /// `category`: `"inverse"` for coin-margined futures.
    /// `symbol`: e.g. `"BTCUSD"` for coin-margined.
    pub async fn get_delivery_price(
        &self,
        category: &str,
        symbol: &str,
        limit: Option<u32>,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }
        self.get(BybitEndpoint::DeliveryPrice, params).await
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// EXCHANGE IDENTITY
// ═══════════════════════════════════════════════════════════════════════════════

impl ExchangeIdentity for BybitConnector {
    fn exchange_id(&self) -> ExchangeId {
        ExchangeId::Bybit
    }

    fn metrics(&self) -> ConnectorStats {
        let (http_requests, http_errors, last_latency_ms) = self.http.stats();
        let (rate_used, rate_max) = if let Ok(mut limiter) = self.limiter.lock() {
            limiter.primary_stats()
        } else {
            (0, 0)
        };
        ConnectorStats {
            http_requests,
            http_errors,
            last_latency_ms,
            rate_used,
            rate_max,
            rate_groups: Vec::new(),
            ws_ping_rtt_ms: 0,
        }
    }

    fn is_testnet(&self) -> bool {
        self.testnet
    }

    fn supported_account_types(&self) -> Vec<AccountType> {
        vec![
            AccountType::Spot,
            AccountType::Margin,
            AccountType::FuturesCross,
            AccountType::FuturesIsolated,
        ]
    }

    fn exchange_type(&self) -> ExchangeType {
        ExchangeType::Cex
    }

    fn rate_limit_capabilities(&self) -> RateLimitCapabilities {
        BYBIT_RATE_CAPS
    }

    fn orderbook_capabilities(&self, account_type: AccountType) -> OrderbookCapabilities {
        static SPOT_CHANNELS: &[WsBookChannel] = &[
            WsBookChannel::snapshot("orderbook.1",    1,    10),
            WsBookChannel::delta("orderbook.50",      Some(50),   Some(20)),
            WsBookChannel::delta("orderbook.200",     Some(200),  Some(100)),
            WsBookChannel::delta("orderbook.1000",    Some(1000), Some(200)),
        ];
        static LINEAR_CHANNELS: &[WsBookChannel] = &[
            WsBookChannel::snapshot("orderbook.1",    1,    10),
            WsBookChannel::delta("orderbook.50",      Some(50),   Some(20)),
            WsBookChannel::delta("orderbook.200",     Some(200),  Some(100)),
            WsBookChannel::delta("orderbook.1000",    Some(1000), Some(200)),
        ];
        static OPTION_CHANNELS: &[WsBookChannel] = &[
            WsBookChannel::delta("orderbook.25",     Some(25),  Some(20)),
            WsBookChannel::delta("orderbook.100",    Some(100), Some(100)),
        ];
        match account_type {
            AccountType::Options => OrderbookCapabilities {
                ws_depths: &[25, 100],
                ws_default_depth: Some(25),
                rest_max_depth: Some(25),
                rest_depth_values: &[],
                supports_snapshot: true,
                supports_delta: true,
                update_speeds_ms: &[20, 100],
                default_speed_ms: Some(20),
                ws_channels: OPTION_CHANNELS,
                checksum: None,
                has_sequence: true,
                has_prev_sequence: false,
                supports_aggregation: false,
                aggregation_levels: &[],
            },
            AccountType::Spot => OrderbookCapabilities {
                ws_depths: &[1, 50, 200, 1000],
                ws_default_depth: Some(50),
                rest_max_depth: Some(200),
                rest_depth_values: &[],
                supports_snapshot: true,
                supports_delta: true,
                update_speeds_ms: &[10, 20, 100, 200],
                default_speed_ms: Some(20),
                ws_channels: SPOT_CHANNELS,
                checksum: None,
                has_sequence: true,
                has_prev_sequence: false,
                supports_aggregation: false,
                aggregation_levels: &[],
            },
            _ => OrderbookCapabilities {
                ws_depths: &[1, 50, 200, 1000],
                ws_default_depth: Some(50),
                rest_max_depth: Some(500),
                rest_depth_values: &[],
                supports_snapshot: true,
                supports_delta: true,
                update_speeds_ms: &[10, 20, 100, 200],
                default_speed_ms: Some(20),
                ws_channels: LINEAR_CHANNELS,
                checksum: None,
                has_sequence: true,
                has_prev_sequence: false,
                supports_aggregation: false,
                aggregation_levels: &[],
            },
        }
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl MarketData for BybitConnector {
    async fn get_price(
        &self,
        symbol: SymbolInput<'_>,
        account_type: AccountType,
    ) -> ExchangeResult<Price> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        params.insert("symbol".to_string(), symbol.to_string());

        let response = self.get(BybitEndpoint::Ticker, params).await?;
        let ticker = BybitParser::parse_ticker(&response)?;
        Ok(ticker.last_price)
    }

    async fn get_orderbook(
        &self,
        symbol: SymbolInput<'_>,
        depth: Option<u16>,
        account_type: AccountType,
    ) -> ExchangeResult<OrderBook> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        params.insert("symbol".to_string(), symbol.to_string());

        if let Some(d) = depth {
            params.insert("limit".to_string(), d.to_string());
        }

        let response = self.get(BybitEndpoint::Orderbook, params).await?;
        BybitParser::parse_orderbook(&response)
    }

    async fn get_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u16>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        params.insert("interval".to_string(), map_kline_interval(interval).to_string());

        if let Some(l) = limit {
            params.insert("limit".to_string(), l.min(1000).to_string());
        }

        if let Some(et) = end_time {
            params.insert("end".to_string(), et.to_string());
        }

        let response = self.get(BybitEndpoint::Klines, params).await?;
        BybitParser::parse_klines(&response)
    }

    async fn get_ticker(
        &self,
        symbol: SymbolInput<'_>,
        account_type: AccountType,
    ) -> ExchangeResult<Ticker> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        params.insert("symbol".to_string(), symbol.to_string());

        let response = self.get(BybitEndpoint::Ticker, params).await?;
        BybitParser::parse_ticker(&response)
    }

    async fn ping(&self) -> ExchangeResult<()> {
        let response = self.get(BybitEndpoint::ServerTime, HashMap::new()).await?;
        self.check_response(&response)
    }

    async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<crate::core::types::SymbolInfo>> {
        let response = self.get_symbols(account_type).await?;
        let symbols = BybitParser::parse_exchange_info(&response, account_type)?;
        self.precision.load_from_symbols(&symbols);
        Ok(symbols)
    }

    fn market_data_capabilities(&self, _account_type: AccountType) -> MarketDataCapabilities {
        // Bybit uses the same /v5/market/* endpoints for both Spot and Futures —
        // the `category` parameter (spot/linear) is passed per-request. Intervals
        // and limits are identical across categories, so no branching needed here.
        MarketDataCapabilities {
            has_ping: true,
            has_price: true,
            has_ticker: true,
            has_orderbook: true,
            has_klines: true,
            has_exchange_info: true,
            has_recent_trades: true,
            has_ws_klines: true,
            has_ws_trades: true,
            has_ws_orderbook: true,
            has_ws_ticker: true,
            // map_kline_interval covers: 1m 3m 5m 15m 30m 1h 2h 4h 6h 12h 1d 1w 1M (no 8h/3d)
            supported_intervals: &[
                "1m", "3m", "5m", "15m", "30m",
                "1h", "2h", "4h", "6h", "12h",
                "1d", "1w", "1M",
            ],
            // get_klines caps limit at .min(1000)
            max_kline_limit: Some(1000),
        }
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl Trading for BybitConnector {
    async fn place_order(&self, req: OrderRequest) -> ExchangeResult<PlaceOrderResponse> {
        let symbol = req.symbol.clone();
        let side = req.side;
        let quantity = req.quantity;
        let account_type = req.account_type;
        let symbol_str = format_symbol(&symbol, account_type);

        match req.order_type {
            OrderType::Market => {
                let order_link_id = format!("cc_{}", crate::core::timestamp_millis());
                
                        let body = json!({
                            "category": account_type_to_category(account_type),
                            "symbol": format_symbol(&symbol, account_type),
                            "side": match side {
                                OrderSide::Buy => "Buy",
                                OrderSide::Sell => "Sell",
                            },
                            "orderType": "Market",
                            "qty": self.precision.qty(&symbol_str, quantity),
                            "orderLinkId": order_link_id,
                        });
                
                        let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                
                        // Extract order ID from response
                        let result = response.get("result")
                            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                
                        let order_id = result.get("orderId")
                            .and_then(|id| id.as_str())
                            .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                            .to_string();
                
                        // Return minimal order info (can fetch full info with get_order)
                        Ok(PlaceOrderResponse::Simple(Order {
                            id: order_id,
                            client_order_id: Some(order_link_id),
                            symbol: Some(symbol.to_string()),
                            side,
                            order_type: OrderType::Market,
                            status: crate::core::OrderStatus::New,
                            price: None,
                            stop_price: None,
                            quantity,
                            filled_quantity: 0.0,
                            average_price: None,
                            commission: None,
                            commission_asset: None,
                            created_at: crate::core::timestamp_millis() as i64,
                            updated_at: None,
                            time_in_force: crate::core::TimeInForce::Gtc,
                        }))
            }
            OrderType::Limit { price } => {
                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));
                let tif = match req.time_in_force {
                    crate::core::TimeInForce::Gtc => "GTC",
                    crate::core::TimeInForce::Ioc => "IOC",
                    crate::core::TimeInForce::Fok => "FOK",
                    crate::core::TimeInForce::PostOnly => "PostOnly",
                    _ => "GTC",
                };

                let mut body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": "Limit",
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "price": self.precision.price(&symbol_str, price),
                    "timeInForce": tif,
                    "orderLinkId": order_link_id,
                });
                if req.reduce_only {
                    body["reduceOnly"] = json!(true);
                }

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;

                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;

                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::Limit { price },
                    status: crate::core::OrderStatus::New,
                    price: Some(price),
                    stop_price: None,
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: req.time_in_force,
                }))
            }
            OrderType::StopMarket { stop_price } => {
                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));

                let mut body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": "Market",
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "triggerPrice": self.precision.price(&symbol_str, stop_price),
                    "triggerBy": "MarkPrice",
                    "orderLinkId": order_link_id,
                });
                if req.reduce_only {
                    body["reduceOnly"] = json!(true);
                }

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::StopMarket { stop_price },
                    status: crate::core::OrderStatus::New,
                    price: None,
                    stop_price: Some(stop_price),
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: crate::core::TimeInForce::Gtc,
                }))
            }
            OrderType::StopLimit { stop_price, limit_price } => {
                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));

                let mut body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": "Limit",
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "price": self.precision.price(&symbol_str, limit_price),
                    "triggerPrice": self.precision.price(&symbol_str, stop_price),
                    "triggerBy": "MarkPrice",
                    "orderLinkId": order_link_id,
                });
                if req.reduce_only {
                    body["reduceOnly"] = json!(true);
                }

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::StopLimit { stop_price, limit_price },
                    status: crate::core::OrderStatus::New,
                    price: Some(limit_price),
                    stop_price: Some(stop_price),
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: crate::core::TimeInForce::Gtc,
                }))
            }
            OrderType::TrailingStop { callback_rate, activation_price } => {
                // Bybit Futures: trailingStop order via conditional order
                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "TrailingStop not supported for Spot/Margin on Bybit".to_string()
                        ));
                    }
                    _ => {}
                }

                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));

                let mut body = json!({
                    "category": "linear",
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": "Market",
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "trailingStop": callback_rate.to_string(),
                    "orderLinkId": order_link_id,
                });
                if let Some(ap) = activation_price {
                    body["activePrice"] = json!(self.precision.price(&symbol_str, ap));
                }
                if req.reduce_only {
                    body["reduceOnly"] = json!(true);
                }

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::TrailingStop { callback_rate, activation_price },
                    status: crate::core::OrderStatus::New,
                    price: None,
                    stop_price: activation_price,
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: crate::core::TimeInForce::Gtc,
                }))
            }
            OrderType::PostOnly { price } => {
                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));

                let body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": "Limit",
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "price": self.precision.price(&symbol_str, price),
                    "timeInForce": "PostOnly",
                    "orderLinkId": order_link_id,
                });

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::PostOnly { price },
                    status: crate::core::OrderStatus::New,
                    price: Some(price),
                    stop_price: None,
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: crate::core::TimeInForce::PostOnly,
                }))
            }
            OrderType::Ioc { price } => {
                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));

                let body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": if price.is_some() { "Limit" } else { "Market" },
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "price": price.map(|p| self.precision.price(&symbol_str, p)).unwrap_or_default(),
                    "timeInForce": "IOC",
                    "orderLinkId": order_link_id,
                });

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::Ioc { price },
                    status: crate::core::OrderStatus::New,
                    price,
                    stop_price: None,
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: crate::core::TimeInForce::Ioc,
                }))
            }
            OrderType::Fok { price } => {
                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));

                let body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": "Limit",
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "price": self.precision.price(&symbol_str, price),
                    "timeInForce": "FOK",
                    "orderLinkId": order_link_id,
                });

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::Fok { price },
                    status: crate::core::OrderStatus::New,
                    price: Some(price),
                    stop_price: None,
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: crate::core::TimeInForce::Fok,
                }))
            }
            OrderType::Gtd { .. } => {
                // GTD (Good-Till-Date) is NOT supported by Bybit v5.
                // Bybit TimeInForce enum: GTC, IOC, FOK, PostOnly, RPI — no GTD.
                // Research confirmed: RESEARCH_WAVE2_BATCH1.md §2.1 "GTD on Bybit: NOT SUPPORTED"
                Err(ExchangeError::NotImplemented(
                    "GTD orders are not supported on Bybit (not in TimeInForce enum)".to_string()
                ))
            }
            OrderType::ReduceOnly { price } => {
                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "ReduceOnly not supported for Spot/Margin".to_string()
                        ));
                    }
                    _ => {}
                }

                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));

                let body = json!({
                    "category": "linear",
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": if price.is_some() { "Limit" } else { "Market" },
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "price": price.map(|p| self.precision.price(&symbol_str, p)).unwrap_or_default(),
                    "reduceOnly": true,
                    "orderLinkId": order_link_id,
                });

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::ReduceOnly { price },
                    status: crate::core::OrderStatus::New,
                    price,
                    stop_price: None,
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: crate::core::TimeInForce::Gtc,
                }))
            }
            OrderType::Iceberg { price, display_quantity } => {
                // Bybit v5 supports iceberg orders via /v5/order/create.
                // Parameters: orderType=Limit, timeInForce=GTC, qty=total, peakOrderQty=visible slice.
                // Research confirmed: RESEARCH_WAVE2_BATCH1.md §2.1 "Iceberg on Bybit: supported"
                let order_link_id = req.client_order_id.clone()
                    .unwrap_or_else(|| format!("cc_{}", crate::core::timestamp_millis()));

                let body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "side": match side {
                        OrderSide::Buy => "Buy",
                        OrderSide::Sell => "Sell",
                    },
                    "orderType": "Limit",
                    "qty": self.precision.qty(&symbol_str, quantity),
                    "price": self.precision.price(&symbol_str, price),
                    "timeInForce": "GTC",
                    "peakOrderQty": self.precision.qty(&symbol_str, display_quantity),
                    "orderLinkId": order_link_id,
                });

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                let result = response.get("result")
                    .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
                let order_id = result.get("orderId")
                    .and_then(|id| id.as_str())
                    .ok_or_else(|| ExchangeError::Parse("Missing orderId".to_string()))?
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id,
                    client_order_id: Some(order_link_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::Iceberg { price, display_quantity },
                    status: crate::core::OrderStatus::New,
                    price: Some(price),
                    stop_price: None,
                    quantity,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: crate::core::timestamp_millis() as i64,
                    updated_at: None,
                    time_in_force: crate::core::TimeInForce::Gtc,
                }))
            }
            // OCO: UI only on Bybit — "API users won't have access to OCO orders"
            // Bracket: no native bracket order type on Bybit API
            // TWAP: UI-only strategy feature, no public API endpoint
            // Research confirmed: RESEARCH_WAVE2_BATCH1.md §2.1
            OrderType::Oco { .. } => Err(ExchangeError::NotImplemented(
                "OCO orders are not available via Bybit API (UI only)".to_string()
            )),
            OrderType::Bracket { .. } => Err(ExchangeError::NotImplemented(
                "Bracket orders are not supported on Bybit API (no native bracket type)".to_string()
            )),
            OrderType::Twap { .. } => Err(ExchangeError::NotImplemented(
                "TWAP orders are not available via Bybit API (UI-only strategy feature)".to_string()
            )),
            _ => Err(ExchangeError::NotImplemented(
                "This order type is not supported by Bybit".to_string()
            )),
        }
    }

    async fn get_order_history(
        &self,
        filter: OrderHistoryFilter,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<Order>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());

        if let Some(ref s) = filter.symbol {
            params.insert("symbol".to_string(), format_symbol(s, account_type));
        }
        if let Some(st) = filter.start_time {
            params.insert("startTime".to_string(), st.to_string());
        }
        if let Some(et) = filter.end_time {
            params.insert("endTime".to_string(), et.to_string());
        }
        if let Some(lim) = filter.limit {
            params.insert("limit".to_string(), lim.min(50).to_string());
        }

        let response = self.get(BybitEndpoint::OrderHistory, params).await?;

        let result = response.get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
        let list = result.get("list")
            .and_then(|l| l.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing list".to_string()))?;

        let mut orders = Vec::new();
        for order_data in list {
            let wrapper = serde_json::json!({
                "retCode": 0,
                "retMsg": "OK",
                "result": order_data,
            });
            if let Ok(order) = BybitParser::parse_order(&wrapper) {
                orders.push(order);
            }
        }

        Ok(orders)
    }

    async fn cancel_order(&self, req: CancelRequest) -> ExchangeResult<Order> {
        match req.scope {
            CancelScope::Single { ref order_id } => {
                let symbol = req.symbol.as_ref()
                    .ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for cancel".into()))?
                    .clone();
                let account_type = req.account_type;

                let body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "orderId": order_id,
                });

                let response = self.post(BybitEndpoint::CancelOrder, body).await?;
                self.check_response(&response)?;

                Ok(Order {
                    id: order_id.to_string(),
                    client_order_id: None,
                    symbol: Some(symbol.to_string()),
                    side: OrderSide::Buy,
                    order_type: OrderType::Limit { price: 0.0 },
                    status: crate::core::OrderStatus::Canceled,
                    price: None,
                    stop_price: None,
                    quantity: 0.0,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: 0,
                    updated_at: Some(crate::core::timestamp_millis() as i64),
                    time_in_force: crate::core::TimeInForce::Gtc,
                })
            }
            CancelScope::All { ref symbol } => {
                let sym = symbol.as_ref()
                    .ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for cancel-all on Bybit".into()))?;
                let account_type = req.account_type;

                let body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(sym, account_type),
                });

                let response = self.post(BybitEndpoint::CancelAllOrders, body).await?;
                self.check_response(&response)?;

                // Return a sentinel cancelled order
                Ok(Order {
                    id: "cancel-all".to_string(),
                    client_order_id: None,
                    symbol: Some(sym.to_string()),
                    side: OrderSide::Buy,
                    order_type: OrderType::Market,
                    status: crate::core::OrderStatus::Canceled,
                    price: None,
                    stop_price: None,
                    quantity: 0.0,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: 0,
                    updated_at: Some(crate::core::timestamp_millis() as i64),
                    time_in_force: crate::core::TimeInForce::Gtc,
                })
            }
            CancelScope::BySymbol { ref symbol } => {
                let account_type = req.account_type;

                let body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(symbol, account_type),
                });

                let response = self.post(BybitEndpoint::CancelAllOrders, body).await?;
                self.check_response(&response)?;

                Ok(Order {
                    id: "cancel-by-symbol".to_string(),
                    client_order_id: None,
                    symbol: Some(symbol.to_string()),
                    side: OrderSide::Buy,
                    order_type: OrderType::Market,
                    status: crate::core::OrderStatus::Canceled,
                    price: None,
                    stop_price: None,
                    quantity: 0.0,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: 0,
                    updated_at: Some(crate::core::timestamp_millis() as i64),
                    time_in_force: crate::core::TimeInForce::Gtc,
                })
            }
            CancelScope::Batch { ref order_ids } => {
                // Bybit V5 does not have a native batch cancel — cancel one by one
                // Per rules: must NOT loop cancel. Return NotImplemented.
                let _ = order_ids;
                Err(ExchangeError::NotImplemented(
                    "Batch cancel not natively supported on Bybit V5 (no atomic batch-cancel endpoint)".to_string()
                ))
            }
            _ => Err(ExchangeError::NotImplemented(
                "This cancel scope is not supported by Bybit".to_string()
            )),
        }
    }

    async fn get_order(
        &self,
        symbol: &str,
        order_id: &str,
        account_type: AccountType,
    ) -> ExchangeResult<Order> {
        // Parse symbol string into Symbol struct
        let symbol_parts: Vec<&str> = symbol.split('/').collect();
        let symbol = if symbol_parts.len() == 2 {
            crate::core::Symbol::new(symbol_parts[0], symbol_parts[1])
        } else {
            crate::core::Symbol { base: symbol.to_string(), quote: String::new(), raw: Some(symbol.to_string()) }
        };

        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        params.insert("symbol".to_string(), format_symbol(&symbol, account_type));
        params.insert("orderId".to_string(), order_id.to_string());

        let response = self.get(BybitEndpoint::OrderStatus, params).await?;
        BybitParser::parse_order(&response)
    
    }

    async fn get_open_orders(
        &self,
        symbol: Option<&str>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<Order>> {
        // Convert Option<&str> to Option<Symbol>
        let symbol_str = symbol;
        let symbol: Option<crate::core::Symbol> = symbol_str.map(|s| {
            let parts: Vec<&str> = s.split('/').collect();
            if parts.len() == 2 {
                crate::core::Symbol::new(parts[0], parts[1])
            } else {
                crate::core::Symbol { base: s.to_string(), quote: String::new(), raw: Some(s.to_string()) }
            }
        });

        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());

        if let Some(s) = symbol {
            params.insert("symbol".to_string(), format_symbol(&s, account_type));
        }

        let response = self.get(BybitEndpoint::OpenOrders, params).await?;

        // Parse all orders from result.list
        let result = response.get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;

        let list = result.get("list")
            .and_then(|l| l.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing list".to_string()))?;

        let mut orders = Vec::new();
        for order_data in list {
            // Create a wrapper to match parser expectations
            let wrapper = json!({
                "retCode": 0,
                "retMsg": "OK",
                "result": order_data,
            });

            if let Ok(order) = BybitParser::parse_order(&wrapper) {
                orders.push(order);
            }
        }

        Ok(orders)

    }

    async fn get_user_trades(
        &self,
        filter: UserTradeFilter,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<UserTrade>> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());

        if let Some(ref s) = filter.symbol {
            params.insert("symbol".to_string(), s.clone());
        }
        if let Some(ref oid) = filter.order_id {
            params.insert("orderId".to_string(), oid.clone());
        }
        if let Some(st) = filter.start_time {
            params.insert("startTime".to_string(), st.to_string());
        }
        if let Some(et) = filter.end_time {
            params.insert("endTime".to_string(), et.to_string());
        }
        if let Some(lim) = filter.limit {
            params.insert("limit".to_string(), lim.min(100).to_string());
        }

        let response = self.get(BybitEndpoint::MyTrades, params).await?;

        let result = response.get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
        let list = result.get("list")
            .and_then(|l| l.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing list".to_string()))?;

        let trades = list
            .iter()
            .filter_map(|item| BybitParser::parse_user_trade(item).ok())
            .collect();

        Ok(trades)
    }

    fn trading_capabilities(&self, account_type: AccountType) -> TradingCapabilities {
        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
        TradingCapabilities {
            has_market_order: true,
            has_limit_order: true,
            has_stop_market: true,
            has_stop_limit: true,
            // TrailingStop: Futures only — Spot/Margin returns NotImplemented (line 874)
            has_trailing_stop: is_futures,
            // Bracket: no native bracket order type on Bybit API (NotImplemented for all)
            has_bracket: false,
            // OCO: UI only on Bybit — not available via API (NotImplemented for all)
            has_oco: false,
            // AmendOrder: POST /v5/order/amend supports both spot and linear
            has_amend: true,
            // BatchOrders: POST /v5/order/create-batch and cancel-batch support both categories
            has_batch: true,
            // Bybit batch limit is 10 per request (same for spot and futures)
            max_batch_size: Some(10),
            // CancelAll: POST /v5/order/cancel-all supports both spot and linear
            has_cancel_all: true,
            has_user_trades: true,
            has_order_history: true,
        }
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl Account for BybitConnector {
    async fn get_balance(&self, query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
        let _asset = query.asset.clone();
        let account_type = query.account_type;

        let mut params = HashMap::new();
        params.insert("accountType".to_string(), match account_type {
            AccountType::Spot | AccountType::Margin => "UNIFIED",
            AccountType::FuturesCross | AccountType::FuturesIsolated => "CONTRACT",
            _ => "UNIFIED",
        }.to_string());

        let response = self.get(BybitEndpoint::Balance, params).await?;
        BybitParser::parse_balance(&response)
    
    }

    async fn get_account_info(&self, account_type: AccountType) -> ExchangeResult<AccountInfo> {
        let response = self.get(BybitEndpoint::AccountInfo, HashMap::new()).await?;

        // Get balances
        let balances = self.get_balance(BalanceQuery { asset: None, account_type }).await?;

        // Parse account info from response
        let result = response.get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;

        let can_trade = result.get("unifiedMarginStatus")
            .and_then(|s| s.as_i64())
            .map(|s| s == 1)
            .unwrap_or(true);

        Ok(AccountInfo {
            account_type,
            can_trade,
            can_withdraw: true,
            can_deposit: true,
            maker_commission: 0.1, // Default, should be fetched from API
            taker_commission: 0.1,
            balances,
        })
    }

    async fn get_fees(&self, symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
        let mut params = HashMap::new();
        params.insert("category".to_string(), "spot".to_string());
        if let Some(s) = symbol {
            params.insert("symbol".to_string(), s.to_string());
        }

        let response = self.get(BybitEndpoint::FeeRate, params).await?;

        let result = response.get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
        let list = result.get("list")
            .and_then(|l| l.as_array())
            .and_then(|a| a.first())
            .ok_or_else(|| ExchangeError::Parse("Empty fee list".to_string()))?;

        let maker_rate = list.get("makerFeeRate")
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse::<f64>().ok())
            .unwrap_or(0.001);

        let taker_rate = list.get("takerFeeRate")
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse::<f64>().ok())
            .unwrap_or(0.001);

        Ok(FeeInfo {
            maker_rate,
            taker_rate,
            symbol: symbol.map(String::from),
            tier: None,
        })
    }

    fn account_capabilities(&self, account_type: AccountType) -> AccountCapabilities {
        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
        AccountCapabilities {
            has_balances: true,
            has_account_info: true,
            has_fees: true,
            // AccountTransfers: no account_type branching in impl — works for all
            has_transfers: true,
            // SubAccounts: no account_type branching in impl — works for all
            has_sub_accounts: true,
            // CustodialFunds: deposit address, withdraw, deposit/withdrawal history — works for all
            has_deposit_withdraw: true,
            // No MarginTrading trait — no borrow/repay endpoints
            has_margin: false,
            // No earn/staking endpoints implemented
            has_earn_staking: false,
            // FundingHistory: funding payments (SETTLEMENT) only exist for Futures positions
            // Spot/Margin returns NotImplemented from get_funding_rate (line 1729)
            has_funding_history: is_futures,
            // AccountLedger: GET /v5/account/transaction-log — available for all account types
            has_ledger: true,
            // No ConvertSwap trait — no coin conversion endpoints
            has_convert: false,
            // Positions (GET /v5/position/list) are Futures/Perp only — Spot has no positions.
            has_positions: is_futures,
        }
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl Positions for BybitConnector {
    async fn get_positions(&self, query: PositionQuery) -> ExchangeResult<Vec<Position>> {
        let symbol = query.symbol.clone();
        let account_type = query.account_type;

        match account_type {
            AccountType::Spot | AccountType::Margin => {
                return Err(ExchangeError::NotImplemented(
                    "Positions not supported for Spot/Margin".to_string()
                ));
            }
            _ => {}
        }

        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());

        if let Some(ref s) = symbol {
            params.insert("symbol".to_string(), format_symbol(s, account_type));
        }

        let response = self.get(BybitEndpoint::Positions, params).await?;

        // Parse positions from result.list
        let result = response.get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;

        let list = result.get("list")
            .and_then(|l| l.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing list".to_string()))?;

        let mut positions = Vec::new();
        for pos_data in list {
            let symbol_str = pos_data.get("symbol")
                .and_then(|s| s.as_str())
                .unwrap_or("");

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

            // Skip zero positions
            if quantity == 0.0 {
                continue;
            }

            let side = pos_data.get("side")
                .and_then(|s| s.as_str())
                .map(|s| match s {
                    "Buy" => crate::core::PositionSide::Long,
                    "Sell" => crate::core::PositionSide::Short,
                    _ => crate::core::PositionSide::Long,
                })
                .unwrap_or(crate::core::PositionSide::Long);

            let entry_price = pos_data.get("avgPrice")
                .and_then(|p| p.as_str())
                .and_then(|s| s.parse::<f64>().ok())
                .unwrap_or(0.0);

            let unrealized_pnl = pos_data.get("unrealisedPnl")
                .and_then(|p| p.as_str())
                .and_then(|s| s.parse::<f64>().ok())
                .unwrap_or(0.0);

            let leverage = pos_data.get("leverage")
                .and_then(|l| l.as_str())
                .and_then(|s| s.parse::<u32>().ok())
                .unwrap_or(1);

            let liquidation_price = pos_data.get("liqPrice")
                .and_then(|p| p.as_str())
                .and_then(|s| s.parse::<f64>().ok());

            let mark_price = pos_data.get("markPrice")
                .and_then(|p| p.as_str())
                .and_then(|s| s.parse::<f64>().ok());

            let margin_type = match account_type {
                AccountType::FuturesCross => crate::core::MarginType::Cross,
                AccountType::FuturesIsolated => crate::core::MarginType::Isolated,
                _ => crate::core::MarginType::Cross,
            };

            positions.push(Position {
                symbol: symbol_str.to_string(),
                side,
                quantity,
                entry_price,
                mark_price,
                unrealized_pnl,
                realized_pnl: None,
                liquidation_price,
                leverage,
                margin_type,
                margin: None,
                take_profit: None,
                stop_loss: None,
            });
        }

        Ok(positions)
    
    }

    async fn get_funding_rate(
        &self,
        symbol: &str,
        account_type: AccountType,
    ) -> ExchangeResult<FundingRate> {
        // Parse symbol string into Symbol struct
        let symbol_str = symbol;
        let symbol = {
            let parts: Vec<&str> = symbol_str.split('/').collect();
            if parts.len() == 2 {
                crate::core::Symbol::new(parts[0], parts[1])
            } else {
                crate::core::Symbol { base: symbol_str.to_string(), quote: String::new(), raw: Some(symbol_str.to_string()) }
            }
        };

        match account_type {
            AccountType::Spot | AccountType::Margin => {
                return Err(ExchangeError::NotImplemented(
                    "Funding rate not supported for Spot/Margin".to_string()
                ));
            }
            _ => {}
        }

        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        params.insert("symbol".to_string(), format_symbol(&symbol, account_type));

        let response = self.get(BybitEndpoint::FundingRate, params).await?;
        BybitParser::parse_funding_rate(&response)

    }

    async fn get_mark_price(
        &self,
        symbol: &str,
    ) -> ExchangeResult<MarkPrice> {
        // GET /v5/market/tickers?category=linear&symbol=BTCUSDT
        // Response: {result: {list: [{symbol, markPrice, indexPrice, fundingRate, ...}]}}
        let mut params = HashMap::new();
        params.insert("category".to_string(), "linear".to_string());
        params.insert("symbol".to_string(), symbol.to_string());

        let response = self.get(BybitEndpoint::Ticker, params).await?;

        let result = response
            .get("result")
            .ok_or_else(|| ExchangeError::Parse("Missing result".to_string()))?;
        let list = result
            .get("list")
            .and_then(|l| l.as_array())
            .ok_or_else(|| ExchangeError::Parse("Missing result.list".to_string()))?;
        let data = list
            .first()
            .ok_or_else(|| ExchangeError::Parse("Empty result.list".to_string()))?;

        let mark_price = data
            .get("markPrice")
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse::<f64>().ok())
            .ok_or_else(|| ExchangeError::Parse("Missing markPrice".to_string()))?;

        Ok(MarkPrice {
            mark_price,
            index_price: data
                .get("indexPrice")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<f64>().ok()),
            funding_rate: data
                .get("fundingRate")
                .and_then(|v| v.as_str())
                .and_then(|s| s.parse::<f64>().ok()),
            timestamp: crate::core::timestamp_millis() as i64, ..Default::default() 
        })
    }

    async fn get_open_interest(
        &self,
        symbol: &str,
        account_type: AccountType,
    ) -> ExchangeResult<OpenInterest> {
        let parts: Vec<&str> = symbol.split('/').collect();
        let raw_symbol = if parts.len() == 2 {
            let sym = crate::core::Symbol::new(parts[0], parts[1]);
            format_symbol(&sym, account_type)
        } else {
            symbol.to_uppercase()
        };

        let mut params = HashMap::new();
        params.insert("category".to_string(), account_type_to_category(account_type).to_string());
        params.insert("symbol".to_string(), raw_symbol.clone());

        let response = self.get(BybitEndpoint::Ticker, params).await?;

        let list = response
            .get("result")
            .and_then(|r| r.get("list"))
            .and_then(|l| l.as_array())
            .ok_or_else(|| ExchangeError::Parse("Bybit OI: missing result.list".to_string()))?;

        let item = list.first()
            .ok_or_else(|| ExchangeError::Parse("Bybit OI: empty list".to_string()))?;

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

        let ts = item.get("time")
            .and_then(|v| v.as_i64())
            .unwrap_or_else(|| crate::core::timestamp_millis() as i64);

        Ok(OpenInterest {
            open_interest: oi,
            open_interest_value: None,
            timestamp: ts, ..Default::default() 
        })
    }

    async fn modify_position(&self, req: PositionModification) -> ExchangeResult<()> {
        match req {
            PositionModification::SetLeverage { ref symbol, leverage, account_type } => {
                let symbol = symbol.clone();

                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "Leverage not supported for Spot/Margin".to_string()
                        ));
                    }
                    _ => {}
                }

                let body = json!({
                    "category": account_type_to_category(account_type),
                    "symbol": format_symbol(&symbol, account_type),
                    "buyLeverage": leverage.to_string(),
                    "sellLeverage": leverage.to_string(),
                });

                let response = self.post(BybitEndpoint::SetLeverage, body).await?;
                self.check_response(&response)?;
                Ok(())
            }
            PositionModification::SetMarginMode { ref symbol, margin_type, account_type } => {
                let symbol = symbol.clone();

                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "SetMarginMode not supported for Spot/Margin".to_string()
                        ));
                    }
                    _ => {}
                }

                let trade_mode = match margin_type {
                    MarginType::Cross => 0i32,
                    MarginType::Isolated => 1i32,
                };

                let body = json!({
                    "category": "linear",
                    "symbol": format_symbol(&symbol, account_type),
                    "tradeMode": trade_mode,
                    "buyLeverage": "1",
                    "sellLeverage": "1",
                });

                let response = self.post(BybitEndpoint::SetMarginMode, body).await?;
                self.check_response(&response)?;
                Ok(())
            }
            PositionModification::AddMargin { ref symbol, amount, account_type } => {
                let symbol = symbol.clone();

                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "AddMargin not supported for Spot/Margin".to_string()
                        ));
                    }
                    _ => {}
                }

                let body = json!({
                    "category": "linear",
                    "symbol": format_symbol(&symbol, account_type),
                    "margin": amount.to_string(),
                    "positionIdx": 0,
                });

                let response = self.post(BybitEndpoint::AddMargin, body).await?;
                self.check_response(&response)?;
                Ok(())
            }
            PositionModification::RemoveMargin { ref symbol, amount, account_type } => {
                let symbol = symbol.clone();

                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "RemoveMargin not supported for Spot/Margin".to_string()
                        ));
                    }
                    _ => {}
                }

                // Bybit: negative margin amount means remove
                let body = json!({
                    "category": "linear",
                    "symbol": format_symbol(&symbol, account_type),
                    "margin": format!("-{}", amount),
                    "positionIdx": 0,
                });

                let response = self.post(BybitEndpoint::AddMargin, body).await?;
                self.check_response(&response)?;
                Ok(())
            }
            PositionModification::ClosePosition { ref symbol, account_type } => {
                let symbol = symbol.clone();

                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "ClosePosition not supported for Spot/Margin".to_string()
                        ));
                    }
                    _ => {}
                }

                let order_link_id = format!("close_{}", crate::core::timestamp_millis());
                let body = json!({
                    "category": "linear",
                    "symbol": format_symbol(&symbol, account_type),
                    "side": "Sell", // Will be auto-corrected by reduceOnly logic
                    "orderType": "Market",
                    "qty": "0",
                    "reduceOnly": true,
                    "closeOnTrigger": true,
                    "orderLinkId": order_link_id,
                });

                let response = self.post(BybitEndpoint::PlaceOrder, body).await?;
                self.check_response(&response)?;
                Ok(())
            }
            PositionModification::SetTpSl { ref symbol, take_profit, stop_loss, account_type } => {
                let symbol = symbol.clone();

                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "SetTpSl not supported for Spot/Margin".to_string()
                        ));
                    }
                    _ => {}
                }

                let mut body = json!({
                    "category": "linear",
                    "symbol": format_symbol(&symbol, account_type),
                    "positionIdx": 0,
                    "tpslMode": "Full",
                });

                if let Some(tp) = take_profit {
                    body["takeProfit"] = json!(tp.to_string());
                }
                if let Some(sl) = stop_loss {
                    body["stopLoss"] = json!(sl.to_string());
                }

                let response = self.post(BybitEndpoint::TpSlMode, body).await?;
                self.check_response(&response)?;
                Ok(())
            }
            _ => Err(ExchangeError::NotImplemented(
                "This position modification is not supported by Bybit".to_string()
            )),
        }
    }

    async fn get_long_short_ratio(
        &self,
        symbol: &str,
        account_type: AccountType,
    ) -> ExchangeResult<crate::core::types::LongShortRatio> {
        let category = account_type_to_category(account_type);
        let vec = self.get_long_short_ratio(category, symbol, "5min", Some(1)).await?;
        vec.into_iter().next().ok_or_else(|| {
            crate::core::types::ExchangeError::NotFound(
                format!("No long/short ratio data for {symbol}"),
            )
        })
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// CANCEL ALL
// ═══════════════════════════════════════════════════════════════════════════════

/// Cancel all open orders via Bybit native endpoint.
///
/// Bybit: `POST /v5/order/cancel-all`
/// Supports both spot and linear (futures).
/// `CancelScope::All { symbol: None }` cancels across the entire category.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CancelAll for BybitConnector {
    async fn cancel_all_orders(
        &self,
        scope: CancelScope,
        account_type: AccountType,
    ) -> ExchangeResult<CancelAllResponse> {
        let symbol = match &scope {
            CancelScope::All { symbol } => symbol.clone(),
            CancelScope::BySymbol { symbol } => Some(symbol.clone()),
            _ => {
                return Err(ExchangeError::InvalidRequest(
                    "cancel_all_orders only accepts All or BySymbol scope".to_string()
                ));
            }
        };

        let mut body = json!({
            "category": account_type_to_category(account_type),
        });

        if let Some(sym) = symbol {
            body["symbol"] = json!(format_symbol(&sym, account_type));
        }

        let response = self.post(BybitEndpoint::CancelAllOrders, body).await?;
        BybitParser::parse_cancel_all_response(&response)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// AMEND ORDER
// ═══════════════════════════════════════════════════════════════════════════════

/// Modify a live order in-place via Bybit native amend endpoint.
///
/// Bybit: `POST /v5/order/amend`
/// Supports spot and linear. At least one of price/quantity must be provided.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AmendOrder for BybitConnector {
    async fn amend_order(&self, req: AmendRequest) -> ExchangeResult<Order> {
        if req.fields.price.is_none() && req.fields.quantity.is_none() && req.fields.trigger_price.is_none() {
            return Err(ExchangeError::InvalidRequest(
                "At least one of price, quantity, or trigger_price must be provided for amend".to_string()
            ));
        }

        let account_type = req.account_type;
        let amend_symbol_str = format_symbol(&req.symbol, account_type);
        let mut body = json!({
            "category": account_type_to_category(account_type),
            "symbol": amend_symbol_str.clone(),
            "orderId": req.order_id,
        });

        if let Some(price) = req.fields.price {
            body["price"] = json!(self.precision.price(&amend_symbol_str, price));
        }
        if let Some(qty) = req.fields.quantity {
            body["qty"] = json!(self.precision.qty(&amend_symbol_str, qty));
        }
        if let Some(trigger_price) = req.fields.trigger_price {
            body["triggerPrice"] = json!(self.precision.price(&amend_symbol_str, trigger_price));
        }

        let response = self.post(BybitEndpoint::AmendOrder, body).await?;
        BybitParser::parse_amend_order_response(&response)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// BATCH ORDERS
// ═══════════════════════════════════════════════════════════════════════════════

/// Native batch order placement and cancellation via Bybit batch endpoints.
///
/// Bybit: `POST /v5/order/create-batch` (max 10), `POST /v5/order/cancel-batch` (max 10)
/// Both spot and linear categories are supported.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl BatchOrders for BybitConnector {
    async fn place_orders_batch(
        &self,
        orders: Vec<OrderRequest>,
    ) -> ExchangeResult<Vec<OrderResult>> {
        if orders.is_empty() {
            return Ok(vec![]);
        }

        if orders.len() > self.max_batch_place_size() {
            return Err(ExchangeError::InvalidRequest(
                format!("Batch size {} exceeds Bybit limit of {}", orders.len(), self.max_batch_place_size())
            ));
        }

        let account_type = orders[0].account_type;
        let category = account_type_to_category(account_type);

        let order_list: Vec<serde_json::Value> = orders.iter().map(|req| {
            let mut obj = serde_json::Map::new();
            obj.insert("category".to_string(), json!(category));
            obj.insert("symbol".to_string(), json!(format_symbol(&req.symbol, req.account_type)));
            obj.insert("side".to_string(), json!(match req.side {
                OrderSide::Buy => "Buy",
                OrderSide::Sell => "Sell",
            }));

            let batch_sym_str = format_symbol(&req.symbol, req.account_type);
            match &req.order_type {
                OrderType::Market => {
                    obj.insert("orderType".to_string(), json!("Market"));
                    obj.insert("qty".to_string(), json!(self.precision.qty(&batch_sym_str, req.quantity)));
                }
                OrderType::Limit { price } => {
                    obj.insert("orderType".to_string(), json!("Limit"));
                    obj.insert("qty".to_string(), json!(self.precision.qty(&batch_sym_str, req.quantity)));
                    obj.insert("price".to_string(), json!(self.precision.price(&batch_sym_str, *price)));
                    obj.insert("timeInForce".to_string(), json!("GTC"));
                }
                _ => {
                    obj.insert("orderType".to_string(), json!("Market"));
                    obj.insert("qty".to_string(), json!(self.precision.qty(&batch_sym_str, req.quantity)));
                }
            }

            if req.reduce_only {
                obj.insert("reduceOnly".to_string(), json!(true));
            }
            if let Some(ref cid) = req.client_order_id {
                obj.insert("orderLinkId".to_string(), json!(cid));
            }

            serde_json::Value::Object(obj)
        }).collect();

        let body = json!({
            "category": category,
            "request": order_list,
        });

        let response = self.post(BybitEndpoint::BatchPlaceOrders, body).await?;
        BybitParser::parse_batch_orders_response(&response)
    }

    async fn cancel_orders_batch(
        &self,
        order_ids: Vec<String>,
        symbol: Option<&str>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<OrderResult>> {
        if order_ids.is_empty() {
            return Ok(vec![]);
        }

        if order_ids.len() > self.max_batch_cancel_size() {
            return Err(ExchangeError::InvalidRequest(
                format!("Batch cancel size {} exceeds Bybit limit of {}", order_ids.len(), self.max_batch_cancel_size())
            ));
        }

        let category = account_type_to_category(account_type);
        let sym = symbol.ok_or_else(|| ExchangeError::InvalidRequest(
            "Symbol is required for batch cancel on Bybit".to_string()
        ))?;

        let cancel_list: Vec<serde_json::Value> = order_ids.iter().map(|id| {
            json!({
                "symbol": sym.replace('/', "").to_uppercase(),
                "orderId": id,
            })
        }).collect();

        let body = json!({
            "category": category,
            "request": cancel_list,
        });

        let response = self.post(BybitEndpoint::BatchCancelOrders, body).await?;
        BybitParser::parse_batch_orders_response(&response)
    }

    fn max_batch_place_size(&self) -> usize {
        10 // Bybit limit
    }

    fn max_batch_cancel_size(&self) -> usize {
        10 // Bybit limit
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// BATCH AMEND
// ═══════════════════════════════════════════════════════════════════════════════

impl BybitConnector {
    /// Batch amend multiple orders via `POST /v5/order/amend-batch`.
    ///
    /// Each entry in `amends` should be a JSON object containing:
    /// `category`, `symbol`, `orderId` (or `orderLinkId`), plus at least one of
    /// `price` or `qty`.
    ///
    /// Max 10 orders per batch (Bybit limit).
    ///
    /// Returns the raw JSON response from Bybit.
    pub async fn batch_amend_orders(
        &self,
        amends: Vec<serde_json::Value>,
        account_type: AccountType,
    ) -> ExchangeResult<Value> {
        if amends.is_empty() {
            return Ok(serde_json::Value::Array(vec![]));
        }
        if amends.len() > 10 {
            return Err(ExchangeError::InvalidRequest(
                format!("Batch amend size {} exceeds Bybit limit of 10", amends.len())
            ));
        }

        let category = account_type_to_category(account_type);
        let body = json!({
            "category": category,
            "request": amends,
        });

        self.post(BybitEndpoint::BatchAmendOrders, body).await
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// ACCOUNT TRANSFERS
// ═══════════════════════════════════════════════════════════════════════════════

/// Internal transfers between Bybit account types.
///
/// Bybit: `POST /v5/asset/transfer/inter-transfer`
/// Supports UNIFIED, SPOT, CONTRACT, and FUND account types.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AccountTransfers for BybitConnector {
    async fn transfer(&self, req: TransferRequest) -> ExchangeResult<TransferResponse> {
        // Generate a unique transfer ID (UUID-like using timestamp)
        let transfer_id = format!("t{}", crate::core::timestamp_millis());

        let body = serde_json::json!({
            "transferId": transfer_id,
            "coin": req.asset,
            "amount": req.amount.to_string(),
            "fromAccountType": account_type_to_transfer_type(req.from_account),
            "toAccountType": account_type_to_transfer_type(req.to_account),
        });

        let response = self.post(BybitEndpoint::InterTransfer, body).await?;
        let mut result = BybitParser::parse_transfer_response(&response)?;

        // Fill in the fields that Bybit doesn't echo back in the response
        result.asset = req.asset;
        result.amount = req.amount;
        Ok(result)
    }

    async fn get_transfer_history(
        &self,
        filter: TransferHistoryFilter,
    ) -> ExchangeResult<Vec<TransferResponse>> {
        let mut params = HashMap::new();

        if let Some(st) = filter.start_time {
            params.insert("startTime".to_string(), st.to_string());
        }
        if let Some(et) = filter.end_time {
            params.insert("endTime".to_string(), et.to_string());
        }
        if let Some(lim) = filter.limit {
            params.insert("limit".to_string(), lim.to_string());
        }

        let response = self.get(BybitEndpoint::TransferHistory, params).await?;
        BybitParser::parse_transfer_history(&response)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// CUSTODIAL FUNDS
// ═══════════════════════════════════════════════════════════════════════════════

/// Deposit and withdrawal management for Bybit.
///
/// - Deposit address: `GET /v5/asset/deposit/query-address`
/// - Withdraw: `POST /v5/asset/withdraw/create`
/// - Deposit history: `GET /v5/asset/deposit/query-record`
/// - Withdrawal history: `GET /v5/asset/withdraw/query-record`
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CustodialFunds for BybitConnector {
    async fn get_deposit_address(
        &self,
        asset: &str,
        network: Option<&str>,
    ) -> ExchangeResult<DepositAddress> {
        let mut params = HashMap::new();
        params.insert("coin".to_string(), asset.to_uppercase());

        if let Some(net) = network {
            params.insert("chainType".to_string(), net.to_string());
        }

        let response = self.get(BybitEndpoint::DepositAddress, params).await?;
        BybitParser::parse_deposit_address(&response, asset, network)
    }

    async fn withdraw(&self, req: WithdrawRequest) -> ExchangeResult<WithdrawResponse> {
        let mut body = serde_json::json!({
            "coin": req.asset,
            "amount": req.amount.to_string(),
            "address": req.address,
            "forceChain": 1,
        });

        if let Some(net) = req.network {
            body["chain"] = serde_json::Value::String(net);
        }
        if let Some(tag) = req.tag {
            body["tag"] = serde_json::Value::String(tag);
        }

        let response = self.post(BybitEndpoint::Withdraw, body).await?;
        BybitParser::parse_withdraw_response(&response)
    }

    async fn get_funds_history(
        &self,
        filter: FundsHistoryFilter,
    ) -> ExchangeResult<Vec<FundsRecord>> {
        match filter.record_type {
            FundsRecordType::Deposit => {
                let mut params = HashMap::new();
                if let Some(ref asset) = filter.asset {
                    params.insert("coin".to_string(), asset.clone());
                }
                if let Some(st) = filter.start_time {
                    params.insert("startTime".to_string(), st.to_string());
                }
                if let Some(et) = filter.end_time {
                    params.insert("endTime".to_string(), et.to_string());
                }
                if let Some(lim) = filter.limit {
                    params.insert("limit".to_string(), lim.to_string());
                }

                let response = self.get(BybitEndpoint::DepositHistory, params).await?;
                BybitParser::parse_deposit_history(&response)
            }
            FundsRecordType::Withdrawal => {
                let mut params = HashMap::new();
                if let Some(ref asset) = filter.asset {
                    params.insert("coin".to_string(), asset.clone());
                }
                if let Some(st) = filter.start_time {
                    params.insert("startTime".to_string(), st.to_string());
                }
                if let Some(et) = filter.end_time {
                    params.insert("endTime".to_string(), et.to_string());
                }
                if let Some(lim) = filter.limit {
                    params.insert("limit".to_string(), lim.to_string());
                }

                let response = self.get(BybitEndpoint::WithdrawHistory, params).await?;
                BybitParser::parse_withdrawal_history(&response)
            }
            FundsRecordType::Both => {
                // Bybit has separate endpoints — fetch both and merge
                let deposit_filter = FundsHistoryFilter {
                    record_type: FundsRecordType::Deposit,
                    ..filter.clone()
                };
                let withdrawal_filter = FundsHistoryFilter {
                    record_type: FundsRecordType::Withdrawal,
                    ..filter
                };

                let mut deposits = self.get_funds_history(deposit_filter).await?;
                let withdrawals = self.get_funds_history(withdrawal_filter).await?;
                deposits.extend(withdrawals);
                Ok(deposits)
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// SUB-ACCOUNTS
// ═══════════════════════════════════════════════════════════════════════════════

/// Sub-account management for Bybit.
///
/// - Create: `POST /v5/user/create-sub-member`
/// - List: `GET /v5/user/query-sub-members`
/// - Transfer: `POST /v5/asset/transfer/universal-transfer`
/// - Get balance: `GET /v5/asset/transfer/query-account-coins-balance`
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl SubAccounts for BybitConnector {
    async fn sub_account_operation(
        &self,
        op: SubAccountOperation,
    ) -> ExchangeResult<SubAccountResult> {
        match op {
            SubAccountOperation::Create { label } => {
                let body = serde_json::json!({
                    "username": label,
                    "memberType": 1,  // 1 = normal sub-account
                });

                let response = self.post(BybitEndpoint::CreateSubMember, body).await?;
                BybitParser::parse_create_sub_member(&response)
            }

            SubAccountOperation::List => {
                let response = self.get(BybitEndpoint::ListSubMembers, HashMap::new()).await?;
                BybitParser::parse_list_sub_members(&response)
            }

            SubAccountOperation::Transfer { sub_account_id, asset, amount, to_sub } => {
                // Universal transfer: master ↔ sub-account
                // For master → sub: fromMemberId = master UID (empty = self), toMemberId = sub_account_id
                // For sub → master: fromMemberId = sub_account_id, toMemberId = master UID (empty = self)
                // Bybit universal transfer requires explicit member IDs.
                // We use the fromAccountType/toAccountType as UNIFIED for both sides since
                // we don't have the user's account type preference here.
                let transfer_id = format!("u{}", crate::core::timestamp_millis());

                let (from_member, to_member) = if to_sub {
                    ("".to_string(), sub_account_id.clone())
                } else {
                    (sub_account_id.clone(), "".to_string())
                };

                let body = serde_json::json!({
                    "transferId": transfer_id,
                    "coin": asset,
                    "amount": amount.to_string(),
                    "fromMemberId": from_member,
                    "toMemberId": to_member,
                    "fromAccountType": "UNIFIED",
                    "toAccountType": "UNIFIED",
                });

                let response = self.post(BybitEndpoint::UniversalTransfer, body).await?;
                BybitParser::parse_universal_transfer(&response)
            }

            SubAccountOperation::GetBalance { sub_account_id } => {
                let mut params = HashMap::new();
                params.insert("memberId".to_string(), sub_account_id);
                params.insert("accountType".to_string(), "UNIFIED".to_string());

                let response = self.get(BybitEndpoint::SubAccountBalance, params).await?;
                BybitParser::parse_sub_account_balance(&response)
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// FUNDING HISTORY
// ═══════════════════════════════════════════════════════════════════════════════

/// Funding payment history via `GET /v5/account/transaction-log?type=SETTLEMENT`
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl FundingHistory for BybitConnector {
    async fn get_funding_payments(
        &self,
        filter: FundingFilter,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<FundingPayment>> {
        let mut params: HashMap<String, String> = HashMap::new();
        params.insert("type".to_string(), "SETTLEMENT".to_string());

        let acct_type_str = match account_type {
            AccountType::Spot => "SPOT",
            _ => "UNIFIED",
        };
        params.insert("accountType".to_string(), acct_type_str.to_string());

        if let Some(symbol) = &filter.symbol {
            params.insert("symbol".to_string(), symbol.to_uppercase());
        }
        if let Some(start) = filter.start_time {
            params.insert("startTime".to_string(), start.to_string());
        }
        if let Some(end) = filter.end_time {
            params.insert("endTime".to_string(), end.to_string());
        }
        if let Some(limit) = filter.limit {
            params.insert("limit".to_string(), limit.min(50).to_string());
        }

        let response = self.get(BybitEndpoint::TransactionLog, params).await?;
        BybitParser::parse_funding_payments(&response)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// ACCOUNT LEDGER
// ═══════════════════════════════════════════════════════════════════════════════

/// Full account ledger via `GET /v5/account/transaction-log` (all types).
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AccountLedger for BybitConnector {
    async fn get_ledger(
        &self,
        filter: LedgerFilter,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<LedgerEntry>> {
        let mut params: HashMap<String, String> = HashMap::new();

        let acct_type_str = match account_type {
            AccountType::Spot => "SPOT",
            _ => "UNIFIED",
        };
        params.insert("accountType".to_string(), acct_type_str.to_string());

        if let Some(asset) = &filter.asset {
            params.insert("currency".to_string(), asset.to_uppercase());
        }
        if let Some(start) = filter.start_time {
            params.insert("startTime".to_string(), start.to_string());
        }
        if let Some(end) = filter.end_time {
            params.insert("endTime".to_string(), end.to_string());
        }
        if let Some(limit) = filter.limit {
            params.insert("limit".to_string(), limit.min(50).to_string());
        }

        let response = self.get(BybitEndpoint::TransactionLog, params).await?;
        let mut entries = BybitParser::parse_ledger(&response)?;

        if let Some(ref type_filter) = filter.entry_type {
            entries.retain(|e| &e.entry_type == type_filter);
        }

        Ok(entries)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// MarketDataPublic trait impl
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl MarketDataPublic for BybitConnector {
    async fn get_open_interest_history(
        &self,
        symbol: SymbolInput<'_>,
        period: &str,
        start_time: Option<i64>,
        end_time: Option<i64>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<OpenInterest>> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let category = account_type_to_category(account_type);
        self.get_open_interest(category, &symbol, period, limit, start_time, end_time).await
    }

    async fn get_mark_price_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let category = account_type_to_category(account_type);
        self.get_mark_price_kline(category, &symbol, interval, limit, None, end_time).await
    }

    async fn get_index_price_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let category = account_type_to_category(account_type);
        self.get_index_price_kline(category, &symbol, interval, limit, None, end_time).await
    }

    async fn get_premium_index_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let category = account_type_to_category(account_type);
        self.get_premium_index_kline(category, &symbol, interval, limit, None, end_time).await
    }

    async fn get_long_short_ratio_history(
        &self,
        symbol: SymbolInput<'_>,
        period: &str,
        _start_time: Option<i64>,
        _end_time: Option<i64>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<LongShortRatio>> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        // Bybit's get_long_short_ratio does not support start/end time filtering.
        let category = account_type_to_category(account_type);
        self.get_long_short_ratio(category, &symbol, period, limit).await
    }

    async fn get_funding_rate_history(
        &self,
        symbol: SymbolInput<'_>,
        start_time: Option<i64>,
        end_time: Option<i64>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<FundingRate>> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let category = account_type_to_category(account_type);
        self.get_funding_rate_history(category, &symbol, start_time, end_time, limit).await
    }

    /// Recent public trades.
    ///
    /// Endpoint: GET /v5/market/recent-trade
    /// spot: max 60 entries; linear/inverse: max 1000.
    /// Side field is explicit ("Buy"/"Sell") — no inversion needed.
    async fn get_recent_trades(
        &self,
        symbol: SymbolInput<'_>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<PublicTrade>> {
        let symbol = symbol.resolve(ExchangeId::Bybit, account_type)?;
        let category = account_type_to_category(account_type);

        let mut params = HashMap::new();
        params.insert("category".to_string(), category.to_string());
        params.insert("symbol".to_string(), symbol.to_string());
        if let Some(l) = limit {
            params.insert("limit".to_string(), l.to_string());
        }

        let response = self.get(BybitEndpoint::RecentTrades, params).await?;
        BybitParser::parse_recent_trades(&response)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// HAS CAPABILITIES
// ═══════════════════════════════════════════════════════════════════════════════

impl crate::core::traits::HasCapabilities for BybitConnector {
    fn capabilities(&self) -> crate::core::types::ConnectorCapabilities {
        crate::core::types::ConnectorCapabilities {
            // MarketData
            has_ticker: true,
            has_orderbook: true,
            has_klines: true,
            has_recent_trades: true,
            has_exchange_info: true,
            // MarketDataPublic (verified overrides: get_open_interest_history,
            //   get_mark_price_klines, get_index_price_klines,
            //   get_long_short_ratio_history, get_funding_rate_history)
            has_open_interest_history: true,
            has_mark_price_klines: true,
            has_index_price_klines: true,
            has_premium_index_klines: true,
            has_agg_trades: false,            has_long_short_ratio_history: true,
            has_funding_rate_history: true,
            // has_basis_history: /v5/market/basis returns HTTP 404 on live wire
            // (confirmed 2026-06-04); docs page also 404s. Not implemented.
            has_basis_history: false,
            has_taker_volume_history: false,
            has_liquidation_bucket_history: false,
            has_insurance_fund: true,
            has_liquidation_history: false,
            has_premium_index: false,
            // Trading
            has_market_order: true,
            has_limit_order: true,
            has_open_orders: true,
            has_order_history: true,
            has_user_trades: true,
            // Positions
            has_positions: true,
            has_mark_price: true,
            has_modify_position: true,
            has_closed_pnl: true,
            has_long_short_ratio: true,
            // Operations
            has_cancel_all: true,
            has_amend_order: true,
            has_batch_place: true,
            has_batch_cancel: true,
            max_batch_place_size: 10,
            max_batch_cancel_size: 10,
            // Account
            has_balance: true,
            has_account_info: true,
            has_fees: true,
            has_transfers: true,
            has_deposit_withdraw: true,
            has_sub_accounts: true,
            has_funding_payments: true,
            has_ledger: true,
            // WebSocket
            has_websocket: true,
            has_ws_klines: true,
            has_ws_trades: true,
            has_ws_orderbook: true,
            has_ws_ticker: true,
            has_ws_mark_price: true,
            has_ws_funding_rate: true,
            validation: self.validation_status(),
        }
    }

    fn validation_status(&self) -> Option<&'static crate::core::types::ValidationStamp> {
        crate::core::utils::validation_snapshot::validation_for(crate::core::types::ExchangeId::Bybit)
    }

    fn trade_history_capabilities(&self) -> crate::core::types::TradeHistoryCapabilities {
        use crate::core::types::TradeHistoryTier;
        // FileDump candidate: public.bybit.com daily CSV.gz. No REST cursor
        // of any kind — recent-trade endpoints return only their fixed
        // window no matter what parameters are sent.
        crate::core::types::TradeHistoryCapabilities {
            spot: TradeHistoryTier::RecentOnly { max_trades: 60 },
            futures: TradeHistoryTier::RecentOnly { max_trades: 1000 },
            kline_backpage: true,
        }
    }

    fn kline_interval_capabilities(&self) -> crate::core::types::KlineIntervalCapabilities {
        // Probe 2026-07-08: GET /v5/market/kline?category=spot&interval=1s
        // errors `{"retCode":10001,"retMsg":"Invalid period!"}`. The same
        // request against category=linear (futures) returns real 1s bars.
        // Bybit's documented interval set is otherwise identical across
        // categories — 1/3/5/15/30/60/120/240/360/720 minutes + D/W/M
        // (connector's own `map_kline_interval` in endpoints.rs has no
        // 6h/8h wire mapping beyond 720=12h, matching official docs).
        crate::core::types::KlineIntervalCapabilities {
            spot: &["1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d", "1w", "1M"],
            futures: &[
                "1s", "1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "12h", "1d", "1w",
                "1M",
            ],
        }
    }
}
