//! # HTX Connector
//!
//! Implementation of all core traits for HTX API.
//!
//! ## Core Traits
//! - `ExchangeIdentity` - exchange identification
//! - `MarketData` - market data
//! - `Trading` - trading operations
//! - `Account` - account information
//!
//! ## Extended Methods
//! Additional HTX-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,
    ExchangeError, ExchangeResult,
    Price, Kline, Ticker, OrderBook,
    Order, OrderSide, OrderType, Balance, AccountInfo,
    Position, FundingRate,
    OrderRequest, CancelRequest, CancelScope,
    BalanceQuery, PositionQuery, PositionModification,
    OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
    UserTrade, UserTradeFilter,
    MarketDataCapabilities, TradingCapabilities, AccountCapabilities,
    SymbolInput,
};
use crate::core::traits::{
    ExchangeIdentity, MarketData, Trading, Account, Positions,
};
use crate::core::{CancelAll, BatchOrders, AccountTransfers, CustodialFunds, SubAccounts};
use crate::core::types::{
    ConnectorStats, CancelAllResponse, OrderResult,
    TransferRequest, TransferHistoryFilter, TransferResponse,
    DepositAddress, WithdrawRequest, WithdrawResponse, FundsRecord, FundsHistoryFilter, FundsRecordType,
    SubAccountOperation, SubAccountResult, SubAccount,
    OpenInterest, MarkPrice,
};
use crate::core::utils::{RuntimeLimiter, RateLimitMonitor, RateLimitPressure};
use crate::core::types::{RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, OrderbookCapabilities, WsBookChannel};

use super::endpoints::{HtxUrls, HtxEndpoint, map_kline_interval};
use super::auth::HtxAuth;
use super::parser::HtxParser;

// ─────────────────────────────────────────────────────────────────────────────
// Symbol helpers
// ─────────────────────────────────────────────────────────────────────────────

/// Convert any HTX symbol representation to the linear-swap contract code format `BASE-QUOTE`.
///
/// HTX linear swap endpoints require `BTC-USDT` (uppercase, hyphen separator).
/// The e2e harness may pass a spot-concat symbol (`btcusdt`) when HTX is configured
/// as Spot for REST. This helper normalises the three common input forms:
/// - Already correct: `BTC-USDT` → returned as-is (uppercased)
/// - Slash-separated: `BTC/USDT` → `BTC-USDT`
/// - Spot concat: `btcusdt` → tries common quote suffixes (USDT, BTC, ETH, BNB)
///   to split; falls back to upcased passthrough if no match.
fn to_linear_swap_code(symbol: &str) -> String {
    let upper = symbol.to_uppercase();
    if upper.contains('-') {
        return upper;
    }
    if let Some(pos) = symbol.find('/') {
        let (base, quote) = symbol.split_at(pos);
        return format!("{}-{}", base.to_uppercase(), quote[1..].to_uppercase());
    }
    // Spot-concat: attempt to split on common quote assets (longest first to avoid ambiguity).
    for quote in &["USDT", "BUSD", "USDC", "BTC", "ETH", "BNB"] {
        if let Some(base) = upper.strip_suffix(quote) {
            if !base.is_empty() {
                return format!("{}-{}", base, quote);
            }
        }
    }
    upper
}

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

static HTX_POOLS: &[RestLimitPool] = &[RestLimitPool {
    name: "default",
    max_budget: 100,
    window_seconds: 10,
    is_weight: true,
    has_server_headers: true,
    server_header: Some("X-RateLimit-Used"),
    header_reports_used: true,
}];

static HTX_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
    model: LimitModel::Weight,
    rest_pools: HTX_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
// ═══════════════════════════════════════════════════════════════════════════════

/// HTX connector
pub struct HtxConnector {
    /// HTTP client
    http: HttpClient,
    /// Authentication (None for public methods)
    auth: Option<HtxAuth>,
    /// Testnet mode (HTX doesn't have dedicated testnet)
    testnet: bool,
    /// REST base URL override for proxy / Path-B routing.
    /// When set, replaces both `HtxUrls::base_url(testnet)` and
    /// `HtxUrls::futures_base_url(testnet)` in every request.
    rest_override: Option<String>,
    /// Runtime rate limiter (Weight model: 100 weight per 10 seconds)
    limiter: Arc<Mutex<RuntimeLimiter>>,
    /// Pressure monitor — gates non-essential requests at >= 90%
    monitor: Arc<Mutex<RateLimitMonitor>>,
    /// Cached account ID for spot trading
    account_id: Arc<Mutex<Option<i64>>>,
    /// Per-symbol precision cache for safe price/qty formatting
    precision: crate::core::utils::precision::PrecisionCache,
}

impl HtxConnector {
    /// 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)`, it replaces both `HtxUrls::base_url(testnet)`
    /// and `HtxUrls::futures_base_url(testnet)` in every request.
    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(HtxAuth::new);

        // Sync time with server if we have auth
        if auth.is_some() {
            let base_url = HtxUrls::base_url(testnet);
            let url = format!("{}/v1/common/timestamp", base_url);
            if let Ok(response) = http.get(&url, &HashMap::new()).await {
                if response["status"] == "ok" {
                    if let Some(time_ms) = response["data"].as_i64() {
                        if let Some(ref mut a) = auth {
                            a.sync_time(time_ms);
                        }
                    }
                }
            }
        }

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

        Ok(Self {
            http,
            auth,
            testnet,
            rest_override,
            limiter,
            monitor,
            account_id: Arc::new(Mutex::new(None)),
            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
    // ═══════════════════════════════════════════════════════════════════════════

    /// Update rate limiter from HTX response headers
    ///
    /// HTX reports either spot headers (X-HB-RateLimit-*) or futures headers (ratelimit-*)
    fn update_rate_from_headers(&self, headers: &HeaderMap) {
        // Try spot headers first: X-HB-RateLimit-Requests-Remain / X-HB-RateLimit-Requests-Limit
        let remaining = headers
            .get("X-HB-RateLimit-Requests-Remain")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.parse::<u32>().ok())
            .or_else(|| {
                // Try futures headers: ratelimit-remaining
                headers
                    .get("ratelimit-remaining")
                    .and_then(|v| v.to_str().ok())
                    .and_then(|s| s.parse::<u32>().ok())
            });

        let limit = headers
            .get("X-HB-RateLimit-Requests-Limit")
            .and_then(|v| v.to_str().ok())
            .and_then(|s| s.parse::<u32>().ok())
            .or_else(|| {
                headers
                    .get("ratelimit-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: HtxEndpoint,
        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(),
            });
        }

        // Route to correct base URL based on endpoint; override replaces both spot and futures bases.
        let real_base = match endpoint {
            HtxEndpoint::FuturesTicker
            | HtxEndpoint::FuturesOrderbook
            | HtxEndpoint::FuturesKlines
            | HtxEndpoint::FuturesTrades
            | HtxEndpoint::OpenInterest
            | HtxEndpoint::FundingRateHistory
            | HtxEndpoint::MarkPrice
            | HtxEndpoint::MarkPriceKline
            | HtxEndpoint::EliteAccountRatio
            | HtxEndpoint::HistoricalFundingRate
            | HtxEndpoint::MarkPriceKlineHistory
            | HtxEndpoint::PremiumIndexKlineHistory
            | HtxEndpoint::OpenInterestHistory
            | HtxEndpoint::ElitePositionRatio
            | HtxEndpoint::BasisHistory => HtxUrls::futures_base_url(self.testnet),
            _ => HtxUrls::base_url(self.testnet),
        };
        let path = endpoint.path();

        // For private endpoints, build signed query string
        let query = if endpoint.is_private() {
            let auth = self.auth.as_ref()
                .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
            auth.build_signed_query("GET", "api.huobi.pro", path, &params)
        } else {
            // Public endpoints: simple query string
            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);

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

    /// POST request
    async fn post(
        &self,
        endpoint: HtxEndpoint,
        body: Value,
    ) -> ExchangeResult<Value> {
        self.rate_limit_wait(1, true).await;

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

        // HTX requires auth params in query string even for POST
        let auth = self.auth.as_ref()
            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;

        // Empty params for signature (business params go in body)
        let query = auth.build_signed_query("POST", "api.huobi.pro", path, &HashMap::new());

        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, path, &format!("?{}", query));

        // Add Content-Type header
        let mut headers = HashMap::new();
        headers.insert("Content-Type".to_string(), "application/json".to_string());

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

    /// Get account ID for spot trading
    ///
    /// HTX requires account-id for most trading operations.
    /// This method caches the ID after first call.
    async fn get_account_id(&self) -> ExchangeResult<i64> {
        // Check cache first
        {
            let cached = self.account_id.lock().expect("Mutex poisoned");
            if let Some(id) = *cached {
                return Ok(id);
            }
        }

        // Fetch account list
        let response = self.get(HtxEndpoint::AccountList, HashMap::new()).await?;
        let accounts = HtxParser::parse_account_list(&response)?;

        // Find spot account
        let spot_account = accounts.iter()
            .find(|(_, account_type)| account_type == "spot")
            .ok_or_else(|| ExchangeError::Parse("No spot account found".to_string()))?;

        let id = spot_account.0;

        // Cache it
        {
            let mut cached = self.account_id.lock().expect("Mutex poisoned");
            *cached = Some(id);
        }

        Ok(id)
    }

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

    /// Get all symbols (raw Value)
    ///
    /// Uses the V1 endpoint (`/v1/common/symbols`) which returns the standard
    /// `{"status": "ok", "data": [...]}` envelope that `HtxParser::extract_result_v1`
    /// expects.
    pub async fn get_symbols(&self) -> ExchangeResult<Value> {
        self.get(HtxEndpoint::SymbolsV1, HashMap::new()).await
    }

    /// Get exchange info (parsed symbol list)
    pub async fn get_exchange_info_parsed(&self, account_type: crate::core::types::AccountType) -> ExchangeResult<Vec<crate::core::types::SymbolInfo>> {
        let response = self.get_symbols().await?;
        HtxParser::parse_exchange_info(&response, account_type)
    }

    /// Cancel all orders (struct method — also available via CancelAll trait).
    ///
    /// `symbol` is the raw exchange-native string (e.g. `"btcusdt"`).
    pub async fn cancel_all_orders(&self, symbol: Option<&str>) -> ExchangeResult<Vec<String>> {
        let account_id = self.get_account_id().await?;

        let mut params = HashMap::new();
        params.insert("account-id".to_string(), account_id.to_string());

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

        // Get all open orders first
        let response = self.get(HtxEndpoint::OpenOrders, params.clone()).await?;

        let data = HtxParser::extract_result_v1(&response)?;
        let order_ids: Vec<String> = data.as_array()
            .map(|arr| {
                arr.iter()
                    .filter_map(|v| v["id"].as_i64().map(|id| id.to_string()))
                    .collect()
            })
            .unwrap_or_default();

        // Cancel in batch
        if !order_ids.is_empty() {
            let body = json!({
                "order-ids": order_ids,
            });

            let _ = self.post(HtxEndpoint::CancelAllOrders, body).await?;
        }

        Ok(order_ids)
    }
}

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

impl ExchangeIdentity for HtxConnector {
    fn exchange_id(&self) -> ExchangeId {
        ExchangeId::HTX
    }

    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,
        ]
    }

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

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

    fn orderbook_capabilities(&self, _account_type: AccountType) -> OrderbookCapabilities {
        static HTX_CHANNELS: &[WsBookChannel] = &[
            WsBookChannel::delta("mbp.5",   Some(5),   None      ),
            WsBookChannel::delta("mbp.10",  Some(10),  Some(100) ),
            WsBookChannel::delta("mbp.20",  Some(20),  Some(100) ),
            WsBookChannel::delta("mbp.150", Some(150), Some(100) ),
            WsBookChannel::delta("mbp.400", Some(400), Some(100) ),
            WsBookChannel::snapshot("depth.step0", 150, 100),
            WsBookChannel::snapshot("depth.step1", 20,  100),
            WsBookChannel::snapshot("depth.step2", 20,  100),
            WsBookChannel::snapshot("depth.step3", 20,  100),
            WsBookChannel::snapshot("depth.step4", 20,  100),
            WsBookChannel::snapshot("depth.step5", 20,  100),
        ];
        OrderbookCapabilities {
            ws_depths: &[5, 10, 20, 150, 400],
            ws_default_depth: Some(20),
            rest_max_depth: Some(150),
            rest_depth_values: &[5, 10, 20, 30, 150],
            supports_snapshot: true,
            supports_delta: true,
            update_speeds_ms: &[100],
            default_speed_ms: Some(100),
            ws_channels: HTX_CHANNELS,
            checksum: None,
            has_sequence: true,
            has_prev_sequence: true,
            supports_aggregation: true,
            aggregation_levels: &["step0", "step1", "step2", "step3", "step4", "step5"],
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// 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 HtxConnector {
    async fn get_price(
        &self,
        symbol: SymbolInput<'_>,
        account_type: AccountType,
    ) -> ExchangeResult<Price> {
        let symbol = symbol.resolve(ExchangeId::HTX, account_type)?;
        let mut params = HashMap::new();

        let (endpoint, param_name) = match account_type {
            AccountType::FuturesCross | AccountType::FuturesIsolated => {
                (HtxEndpoint::FuturesTicker, "contract_code")
            }
            _ => (HtxEndpoint::Ticker, "symbol"),
        };

        params.insert(param_name.to_string(), symbol.to_string());

        let response = self.get(endpoint, params).await?;
        let ticker = HtxParser::parse_ticker(&response, &symbol)?;
        Ok(ticker.last_price)
    }

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

        let (endpoint, param_name) = match account_type {
            AccountType::FuturesCross | AccountType::FuturesIsolated => {
                (HtxEndpoint::FuturesOrderbook, "contract_code")
            }
            _ => (HtxEndpoint::Orderbook, "symbol"),
        };

        params.insert(param_name.to_string(), symbol.to_string());
        params.insert("type".to_string(), "step0".to_string());

        if let Some(d) = depth {
            let depth_str = match d {
                1..=5 => "5",
                6..=10 => "10",
                _ => "20",
            };
            params.insert("depth".to_string(), depth_str.to_string());
        }

        let response = self.get(endpoint, params).await?;
        HtxParser::parse_orderbook(&response)
    }

    /// `GET /market/history/kline` (spot) / `.../linear-swap-ex/market/
    /// history/kline` (futures) — genuinely has NO backward pagination.
    ///
    /// Wave 2 investigation (2026-07-08): live-probed `from`/`to` (seconds)
    /// against both the spot and linear-swap kline endpoints — every call
    /// returned the SAME most-recent window as a plain `size`-only request
    /// (identical newest `id`, e.g. requesting a window 200 000s-190 000s in
    /// the past returned bars starting at the current newest bar, not the
    /// requested older window). Only `size` (this connector's `limit`,
    /// clamped to 2000) has any effect on the response. This is a hard
    /// venue-side ceiling, not a bug in this wiring — `_end_time` is left
    /// unused (documented, not silently dropped) because there is nothing
    /// on the wire for it to control.
    /// `trade_history_capabilities().kline_backpage = false` reflects this.
    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::HTX, account_type)?;
        let mut params = HashMap::new();

        let (endpoint, param_name) = match account_type {
            AccountType::FuturesCross | AccountType::FuturesIsolated => {
                (HtxEndpoint::FuturesKlines, "contract_code")
            }
            _ => (HtxEndpoint::Klines, "symbol"),
        };

        params.insert(param_name.to_string(), symbol.to_string());
        params.insert("period".to_string(), map_kline_interval(interval).to_string());

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

        let response = self.get(endpoint, params).await?;
        HtxParser::parse_klines(&response)
    }

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

        let (endpoint, param_name) = match account_type {
            AccountType::FuturesCross | AccountType::FuturesIsolated => {
                (HtxEndpoint::FuturesTicker, "contract_code")
            }
            _ => (HtxEndpoint::Ticker, "symbol"),
        };

        params.insert(param_name.to_string(), symbol.to_string());

        let response = self.get(endpoint, params).await?;
        HtxParser::parse_ticker(&response, &symbol)
    }

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

        if response["status"] == "ok" {
            Ok(())
        } else {
            Err(ExchangeError::Api {
                code: 0,
                message: "Ping failed".to_string(),
            })
        }
    }

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

    fn market_data_capabilities(&self, _account_type: AccountType) -> MarketDataCapabilities {
        MarketDataCapabilities {
            has_ping: true,
            has_price: true,
            has_ticker: true,
            has_orderbook: true,
            has_klines: true,
            has_exchange_info: true,
            // get_recent_trades overrides MarketDataPublic (HistoryTrades / FuturesTrades).
            has_recent_trades: true,
            // HTX intervals: 1min 5min 15min 30min 60min 4hour 1day 1week 1mon 1year
            // "1y" maps to "1year" in endpoints.rs map_kline_interval.
            supported_intervals: &["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "1M", "1y"],
            // HTX kline endpoint accepts up to 2000 bars per request.
            max_kline_limit: Some(2000),
            // HTX WebSocket: market.{symbol}.kline.{period} channel for klines.
            has_ws_klines: true,
            // HTX WebSocket: market.{symbol}.trade.detail channel.
            has_ws_trades: true,
            // HTX WebSocket: market.{symbol}.depth.step0 channel for orderbook.
            has_ws_orderbook: true,
            // HTX WebSocket: market.{symbol}.ticker channel.
            has_ws_ticker: true,
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// HELPERS
// ═══════════════════════════════════════════════════════════════════════════════

/// Convert a Unix timestamp (seconds) to an HTX date string "YYYY-MM-DD".
///
/// HTX matchresults endpoint accepts `start-date` / `end-date` in this format.
fn millis_to_date_str(unix_secs: i64) -> String {
    // Simple manual conversion without external date crates
    // Days since Unix epoch (1970-01-01)
    let days = unix_secs / 86400;
    let mut y = 1970i64;
    let mut remaining_days = days;

    loop {
        let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
        let days_in_year = if leap { 366 } else { 365 };
        if remaining_days < days_in_year {
            break;
        }
        remaining_days -= days_in_year;
        y += 1;
    }

    let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
    let days_per_month: [i64; 12] = [31, if leap { 29 } else { 28 }, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];

    let mut m = 1i64;
    for &dim in &days_per_month {
        if remaining_days < dim {
            break;
        }
        remaining_days -= dim;
        m += 1;
    }

    let d = remaining_days + 1;
    format!("{:04}-{:02}-{:02}", y, m, d)
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl Trading for HtxConnector {
    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 account_id = self.get_account_id().await?;
        let client_order_id = format!("cc_{}", crate::core::timestamp_millis());
        // Caller must pass raw exchange-native symbol in OrderRequest.symbol.raw(),
        // falling back to lowercase-concat as a best-effort spot format.
        let htx_symbol = symbol
            .raw()
            .map(|r| r.to_string())
            .unwrap_or_else(|| format!("{}{}", symbol.base.to_lowercase(), symbol.quote.to_lowercase()));
        let qty_str = self.precision.qty(&htx_symbol, quantity);

        // Helper to map side to HTX order type prefix
        let side_str = match side {
            OrderSide::Buy => "buy",
            OrderSide::Sell => "sell",
        };

        match req.order_type {
            OrderType::Market => {
                let order_type = format!("{}-market", side_str);
                let body = json!({
                    "account-id": account_id.to_string(),
                    "symbol": htx_symbol,
                    "type": order_type,
                    "amount": qty_str,
                    "client-order-id": client_order_id,
                });

                let response = self.post(HtxEndpoint::PlaceOrder, body).await?;
                let order = HtxParser::parse_order(&response)?;

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order.id,
                    client_order_id: Some(client_order_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_type = format!("{}-limit", side_str);
                let body = json!({
                    "account-id": account_id.to_string(),
                    "symbol": htx_symbol,
                    "type": order_type,
                    "amount": qty_str,
                    "price": self.precision.price(&htx_symbol, price),
                    "client-order-id": client_order_id,
                });

                let response = self.post(HtxEndpoint::PlaceOrder, body).await?;
                let order = HtxParser::parse_order(&response)?;

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order.id,
                    client_order_id: Some(client_order_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::Limit { price: 0.0 },
                    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,
                }))
            }

            OrderType::StopLimit { stop_price, limit_price } => {
                // HTX: buy-stop-limit / sell-stop-limit
                // Requires: stop-price, operator (gte/lte), price
                let order_type = format!("{}-stop-limit", side_str);
                // For buy stop: trigger when price >= stop_price (gte)
                // For sell stop: trigger when price <= stop_price (lte)
                let operator = match side {
                    OrderSide::Buy => "gte",
                    OrderSide::Sell => "lte",
                };
                let body = json!({
                    "account-id": account_id.to_string(),
                    "symbol": htx_symbol,
                    "type": order_type,
                    "amount": qty_str,
                    "stop-price": self.precision.price(&htx_symbol, stop_price),
                    "price": self.precision.price(&htx_symbol, limit_price),
                    "operator": operator,
                    "client-order-id": client_order_id,
                });

                let response = self.post(HtxEndpoint::PlaceOrder, body).await?;
                let order = HtxParser::parse_order(&response)?;

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order.id,
                    client_order_id: Some(client_order_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::PostOnly { price } => {
                // HTX: buy-limit-maker / sell-limit-maker (post-only)
                let order_type = format!("{}-limit-maker", side_str);
                let body = json!({
                    "account-id": account_id.to_string(),
                    "symbol": htx_symbol,
                    "type": order_type,
                    "amount": qty_str,
                    "price": self.precision.price(&htx_symbol, price),
                    "client-order-id": client_order_id,
                });

                let response = self.post(HtxEndpoint::PlaceOrder, body).await?;
                let order = HtxParser::parse_order(&response)?;

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order.id,
                    client_order_id: Some(client_order_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::Gtc,
                }))
            }

            OrderType::Ioc { price } => {
                // HTX: buy-ioc / sell-ioc
                let price_val = price.unwrap_or(0.0);
                let order_type = format!("{}-ioc", side_str);
                let body = json!({
                    "account-id": account_id.to_string(),
                    "symbol": htx_symbol,
                    "type": order_type,
                    "amount": qty_str,
                    "price": self.precision.price(&htx_symbol, price_val),
                    "client-order-id": client_order_id,
                });

                let response = self.post(HtxEndpoint::PlaceOrder, body).await?;
                let order = HtxParser::parse_order(&response)?;

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order.id,
                    client_order_id: Some(client_order_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 } => {
                // HTX: buy-limit-fok / sell-limit-fok
                let order_type = format!("{}-limit-fok", side_str);
                let body = json!({
                    "account-id": account_id.to_string(),
                    "symbol": htx_symbol,
                    "type": order_type,
                    "amount": qty_str,
                    "price": self.precision.price(&htx_symbol, price),
                    "client-order-id": client_order_id,
                });

                let response = self.post(HtxEndpoint::PlaceOrder, body).await?;
                let order = HtxParser::parse_order(&response)?;

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order.id,
                    client_order_id: Some(client_order_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,
                }))
            }

            // Trailing stop via POST /v2/algo-orders (API-only feature).
            // orderType: "trailing-stop-order"
            // trailingRate: callback rate (0 < rate <= 5%)
            // activationPrice: optional price at which trailing begins
            OrderType::TrailingStop { callback_rate, activation_price } => {
                let body = json!({
                    "accountId": account_id.to_string(),
                    "symbol": htx_symbol,
                    "orderSide": side_str,
                    "orderSize": qty_str,
                    "orderType": "trailing-stop-order",
                    // trailingRate must be > 0 and <= 5 (as percentage string)
                    "trailingRate": format!("{:.4}", callback_rate.clamp(0.0001, 5.0)),
                    // activationPrice is optional
                    "activationPrice": activation_price
                        .map(|p| self.precision.price(&htx_symbol, p))
                        .unwrap_or_default(),
                });

                let response = self.post(HtxEndpoint::AlgoOrders, body).await?;

                // Algo orders return: { "code": 200, "data": { "clientOrderId": "...", "orderId": "..." } }
                let order_id_str = response
                    .pointer("/data/orderId")
                    .or_else(|| response.pointer("/data/clientOrderId"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                Ok(PlaceOrderResponse::Simple(Order {
                    id: order_id_str,
                    client_order_id: Some(client_order_id),
                    symbol: Some(symbol.to_string()),
                    side,
                    order_type: OrderType::TrailingStop { callback_rate, activation_price },
                    status: crate::core::OrderStatus::New,
                    price: activation_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,
                }))
            }

            _ => Err(ExchangeError::NotImplemented(
                format!("{:?} order type not supported on {:?}", req.order_type, self.exchange_id())
            )),
        }
    }

    async fn cancel_order(&self, req: CancelRequest) -> ExchangeResult<Order> {
        match req.scope {
            CancelScope::Single { ref order_id } => {
                // HTX uses path variable for order ID
                let path = HtxEndpoint::CancelOrder.path_with_vars(&[("order-id", order_id)]);

                let real_base = HtxUrls::base_url(self.testnet);
                let auth = self.auth.as_ref()
                    .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
                let query = auth.build_signed_query("POST", "api.huobi.pro", &path, &HashMap::new());

                let url = assemble_rest_url(self.rest_override.as_deref(), real_base, &path, &format!("?{}", query));

                let body = json!({});
                let mut headers = HashMap::new();
                headers.insert("Content-Type".to_string(), "application/json".to_string());

                self.rate_limit_wait(1, true).await;
                let (response, resp_headers) = self.http.post_with_response_headers(&url, &body, &headers).await?;
                self.update_rate_from_headers(&resp_headers);

                HtxParser::parse_order(&response)
            }

            CancelScope::Batch { ref order_ids } => {
                // HTX: POST /v1/order/orders/batchcancel with {"order-ids": [...]}
                // Max 50 IDs per request
                let body = json!({
                    "order-ids": order_ids,
                });

                let _response = self.post(HtxEndpoint::CancelAllOrders, body).await?;

                // Return placeholder for first order
                Ok(Order {
                    id: order_ids.first().cloned().unwrap_or_default(),
                    client_order_id: None,
                    symbol: req.symbol.as_ref().map(|s| s.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,
                })
            }

            _ => Err(ExchangeError::NotImplemented(
                format!("{:?} cancel scope not supported — use CancelAll trait", req.scope)
            )),
        }
    }

    async fn get_order_history(
        &self,
        filter: OrderHistoryFilter,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<Order>> {
        let mut params = HashMap::new();

        if let Some(sym) = &filter.symbol {
            let raw = sym.raw()
                .map(|r| r.to_string())
                .unwrap_or_else(|| format!("{}{}", sym.base.to_lowercase(), sym.quote.to_lowercase()));
            params.insert("symbol".to_string(), raw);
        }

        // HTX requires states filter
        params.insert("states".to_string(), "filled,canceled".to_string());

        if let Some(start) = filter.start_time {
            params.insert("start-time".to_string(), start.to_string());
        }
        if let Some(end) = filter.end_time {
            params.insert("end-time".to_string(), end.to_string());
        }
        if let Some(limit) = filter.limit {
            params.insert("size".to_string(), limit.min(100).to_string());
        }

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

        let data = HtxParser::extract_result_v1(&response)?;
        let orders = data.as_array()
            .ok_or_else(|| ExchangeError::Parse("Data is not an array".into()))?
            .iter()
            .filter_map(|order_json| {
                let wrapped = json!({"status": "ok", "data": order_json});
                HtxParser::parse_order(&wrapped).ok()
            })
            .collect();

        Ok(orders)
    }

    async fn get_order(
        &self,
        _symbol: &str,
        order_id: &str,
        _account_type: AccountType,
    ) -> ExchangeResult<Order> {
        let path = HtxEndpoint::OrderStatus.path_with_vars(&[("order-id", order_id)]);

        let real_base = HtxUrls::base_url(self.testnet);
        let auth = self.auth.as_ref()
            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
        let query = auth.build_signed_query("GET", "api.huobi.pro", &path, &HashMap::new());

        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, &path, &format!("?{}", query));

        self.rate_limit_wait(1, true).await;
        let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &HashMap::new()).await?;
        self.update_rate_from_headers(&resp_headers);

        HtxParser::parse_order(&response)
    }

    async fn get_open_orders(
        &self,
        symbol: Option<&str>,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<Order>> {
        let account_id = self.get_account_id().await?;

        let mut params = HashMap::new();
        params.insert("account-id".to_string(), account_id.to_string());

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

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

        let data = HtxParser::extract_result_v1(&response)?;
        let orders = data.as_array()
            .ok_or_else(|| ExchangeError::Parse("Data is not an array".into()))?
            .iter()
            .filter_map(|order_json| {
                let wrapped = json!({"status": "ok", "data": order_json});
                HtxParser::parse_order(&wrapped).ok()
            })
            .collect();

        Ok(orders)
    }

    async fn get_user_trades(
        &self,
        filter: UserTradeFilter,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<UserTrade>> {
        let mut params = HashMap::new();

        if let Some(sym) = &filter.symbol {
            // Caller passes raw exchange-native string (e.g. "btcusdt").
            params.insert("symbol".to_string(), sym.clone());
        }

        if let Some(lim) = filter.limit {
            params.insert("size".to_string(), lim.min(500).to_string());
        }

        // HTX matchresults uses date strings (YYYY-MM-DD), not ms timestamps.
        // When ms timestamps are provided, convert to date strings.
        if let Some(start_ms) = filter.start_time {
            let start_secs = (start_ms / 1000) as i64;
            let date = millis_to_date_str(start_secs);
            params.insert("start-date".to_string(), date);
        }
        if let Some(end_ms) = filter.end_time {
            let end_secs = (end_ms / 1000) as i64;
            let date = millis_to_date_str(end_secs);
            params.insert("end-date".to_string(), date);
        }

        let response = self.get(HtxEndpoint::MatchResults, params).await?;
        HtxParser::parse_user_trades(&response)
    }

    fn trading_capabilities(&self, account_type: AccountType) -> TradingCapabilities {
        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);

        if is_futures {
            // Futures (hbdm.com) uses a completely different REST API — none of the
            // spot trading endpoints in this connector are wired for futures.
            // place_order falls through to NotImplemented for all order types.
            // CancelAll calls get_account_id() which returns a spot account ID → would
            // fail on futures. cancel_orders_batch uses CancelAllOrders (spot endpoint).
            // MatchResults and OrderHistory are spot-only endpoints on api.huobi.pro.
            TradingCapabilities {
                has_market_order: false,
                has_limit_order: false,
                has_stop_market: false,
                has_stop_limit: false,
                has_trailing_stop: false,
                has_bracket: false,
                has_oco: false,
                has_amend: false,
                has_batch: false,
                max_batch_size: None,
                has_cancel_all: false,
                has_user_trades: false,
                has_order_history: false,
            }
        } else {
            // Spot / Margin
            TradingCapabilities {
                has_market_order: true,
                has_limit_order: true,
                // HTX spot has no stop-market type; stop orders require a limit price.
                has_stop_market: false,
                has_stop_limit: true,
                // Trailing stop via POST /v2/algo-orders (AlgoOrders endpoint).
                has_trailing_stop: true,
                has_bracket: false,
                has_oco: false,
                // No AmendOrder trait; HTX spot has no native order-amend endpoint.
                has_amend: false,
                // place_batch_orders returns NotImplemented; cancel batch works
                // via CancelAllOrders (up to 50 IDs). has_batch = false because place
                // is unsupported — the flag means full batch round-trip, not cancel-only.
                has_batch: false,
                max_batch_size: None,
                // CancelAll trait via POST /v1/order/orders/batchCancelOpenOrders.
                has_cancel_all: true,
                // MatchResults endpoint: spot fills only.
                has_user_trades: true,
                // OrderHistory endpoint: spot orders only.
                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 HtxConnector {
    async fn get_balance(&self, query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
        let _asset = query.asset.clone();
        let _account_type = query.account_type;
        let account_id = self.get_account_id().await?;

        // Replace path variable
        let path = HtxEndpoint::Balance.path_with_vars(&[("account-id", &account_id.to_string())]);

        let real_base = HtxUrls::base_url(self.testnet);
        let auth = self.auth.as_ref()
            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
        let query = auth.build_signed_query("GET", "api.huobi.pro", &path, &HashMap::new());

        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, &path, &format!("?{}", query));

        self.rate_limit_wait(1, true).await;
        let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &HashMap::new()).await?;
        self.update_rate_from_headers(&resp_headers);

        HtxParser::parse_balance(&response)
    }

    async fn get_account_info(&self, _account_type: AccountType) -> ExchangeResult<AccountInfo> {
        let balances = self.get_balance(BalanceQuery { asset: None, account_type: _account_type }).await?;

        Ok(AccountInfo {
            account_type: _account_type,
            can_trade: true,
            can_withdraw: true,
            can_deposit: true,
            maker_commission: 0.002, // 0.2% default
            taker_commission: 0.002,
            balances,
        })
    }

    async fn get_fees(&self, symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
        // HTX: GET /v2/reference/transact-fee-rate?symbols=btcusdt
        let mut params = HashMap::new();

        if let Some(sym) = symbol {
            // Caller passes raw exchange-native string (e.g. "btcusdt").
            params.insert("symbols".to_string(), sym.to_string());
        }

        let response = self.get(HtxEndpoint::TransactFee, params).await?;

        // HTX v2 response format: {"code": 200, "data": [{"symbol": "btcusdt", "makerFeeRate": "0.002", "takerFeeRate": "0.002"}]}
        let data = response.get("data")
            .and_then(|d| d.as_array())
            .and_then(|arr| arr.first())
            .ok_or_else(|| ExchangeError::Parse("No fee data".to_string()))?;

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

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

        Ok(FeeInfo {
            maker_rate,
            taker_rate,
            symbol: symbol.map(|s| s.to_string()),
            tier: None,
        })
    }

    fn account_capabilities(&self, account_type: AccountType) -> AccountCapabilities {
        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);

        // get_balance / get_account_info use the spot account endpoint (api.huobi.pro).
        // Futures balance lives on api.hbdm.com — not implemented in this connector.
        // has_transfers is true for both: POST /v1/futures/transfer moves funds between
        // spot and futures regardless of which side initiates the call.
        // has_deposit_withdraw and has_sub_accounts are master-account operations,
        // independent of which sub-account type is queried — they work for both.
        // has_fees uses /v2/reference/transact-fee-rate (spot fee table only).
        AccountCapabilities {
            has_balances: !is_futures,
            has_account_info: !is_futures,
            // Fee endpoint is spot-only; futures fees use a different hbdm endpoint.
            has_fees: !is_futures,
            // Transfer endpoint moves funds between spot↔futures; available from either side.
            has_transfers: true,
            // Sub-account operations are master-level (api.huobi.pro), same for both.
            has_sub_accounts: true,
            // Deposit/withdraw are custodial (chain-level), not account-type-specific.
            has_deposit_withdraw: true,
            has_margin: false,
            has_earn_staking: false,
            // No FundingHistory trait; get_funding_rate_history is an inherent method only.
            has_funding_history: false,
            // Ledger endpoint exists (/v2/account/ledger) but no AccountLedger trait implemented.
            has_ledger: false,
            has_convert: false,
            // HTX connector is spot-only — no futures positions implemented.
            has_positions: false,
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// POSITIONS (Spot has no positions)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl Positions for HtxConnector {
    async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
        // Spot trading has no positions
        Ok(vec![])
    }

    async fn get_funding_rate(
        &self,
        _symbol: &str,
        _account_type: AccountType,
    ) -> ExchangeResult<FundingRate> {
        Err(ExchangeError::WireAbsent("Funding rate not available for spot trading".to_string()))
    }

    async fn get_mark_price(
        &self,
        symbol: &str,
    ) -> ExchangeResult<MarkPrice> {
        // GET /index/market/history/linear_swap_mark_price_kline?contract_code=BTC-USDT&period=1min&size=1
        // Response: {status, data: [{id, open, close, high, low, vol, ...}]}
        // Use close price of latest kline as mark price
        let contract_code = to_linear_swap_code(symbol);
        let response = self.get_mark_price_kline(&contract_code, "1min", Some(1)).await?;

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

        let kline = data
            .first()
            .ok_or_else(|| ExchangeError::Parse("Empty data array".to_string()))?;

        let mark_price = kline
            .get("close")
            .and_then(|v| v.as_f64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
            .ok_or_else(|| ExchangeError::Parse("Missing close in mark price kline".to_string()))?;

        Ok(MarkPrice {
            mark_price,
            index_price: None,
            funding_rate: None,
            timestamp: kline
                .get("id")
                .and_then(|v| v.as_i64())
                .map(|t| t * 1000)
                .unwrap_or_else(|| crate::core::timestamp_millis() as i64), ..Default::default() 
        })
    }

    async fn modify_position(&self, req: PositionModification) -> ExchangeResult<()> {
        match req {
            PositionModification::SetLeverage { .. } => {
                Err(ExchangeError::WireAbsent("Leverage not available for spot trading".to_string()))
            }
            _ => Err(ExchangeError::NotImplemented(
                "Position modification not supported on HTX spot".to_string()
            )),
        }
    }

    async fn get_open_interest(
        &self,
        symbol: &str,
        _account_type: AccountType,
    ) -> ExchangeResult<OpenInterest> {
        let contract_code = to_linear_swap_code(symbol);

        let response = self.get_open_interest(Some(&contract_code)).await?;

        let data = response.get("data")
            .and_then(|d| d.as_array())
            .and_then(|a| a.first())
            .ok_or_else(|| ExchangeError::Parse("HTX OI: missing data[0]".to_string()))?;

        let oi = data.get("open_interest")
            .and_then(|v| v.as_f64())
            .or_else(|| data.get("volume").and_then(|v| v.as_f64()))
            .unwrap_or(0.0);

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

        let open_interest_value = data.get("value").and_then(|v| v.as_f64());
        let symbol_field = data.get("contract_code")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());
        let open_interest_ccy = data.get("amount").and_then(|v| v.as_f64());
        let trade_amount = data.get("trade_amount").and_then(|v| v.as_f64());
        let trade_volume = data.get("trade_volume").and_then(|v| v.as_f64());
        let trade_turnover = data.get("trade_turnover").and_then(|v| v.as_f64());

        Ok(OpenInterest {
            open_interest: oi,
            open_interest_value,
            timestamp: ts,
            symbol: symbol_field,
            open_interest_ccy,
            trade_amount,
            trade_volume,
            trade_turnover, ..Default::default()
        })
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// CANCEL ALL (optional trait)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CancelAll for HtxConnector {
    async fn cancel_all_orders(
        &self,
        scope: CancelScope,
        _account_type: AccountType,
    ) -> ExchangeResult<CancelAllResponse> {
        let account_id = self.get_account_id().await?;

        // HTX batchCancelOpenOrders: POST /v1/order/orders/batchCancelOpenOrders
        // Optional fields: account-id, symbol, side
        // Without symbol: cancels ALL open orders across all pairs
        match scope {
            CancelScope::All { symbol: None } => {
                // Cancel all open orders — no symbol filter
                let body = json!({
                    "account-id": account_id.to_string(),
                });

                let response = self.post(HtxEndpoint::CancelOpenOrders, body).await?;

                // HTX returns {"status": "ok", "data": {"success-count": N, "failed-count": M, "next-id": -1}}
                let data = HtxParser::extract_result_v1(&response)?;
                let cancelled_count = data.get("success-count")
                    .and_then(|v| v.as_u64())
                    .map(|n| n as u32)
                    .unwrap_or(0);
                let failed_count = data.get("failed-count")
                    .and_then(|v| v.as_u64())
                    .map(|n| n as u32)
                    .unwrap_or(0);

                Ok(CancelAllResponse {
                    cancelled_count,
                    failed_count,
                    details: vec![],
                })
            }

            CancelScope::All { symbol: Some(sym) } | CancelScope::BySymbol { symbol: sym } => {
                // Cancel all open orders for a specific symbol.
                // Use raw field if set; otherwise fall back to lowercase concat (spot format).
                let htx_symbol = sym.raw()
                    .map(|r| r.to_string())
                    .unwrap_or_else(|| format!("{}{}", sym.base.to_lowercase(), sym.quote.to_lowercase()));
                let body = json!({
                    "account-id": account_id.to_string(),
                    "symbol": htx_symbol,
                });

                let response = self.post(HtxEndpoint::CancelOpenOrders, body).await?;

                let data = HtxParser::extract_result_v1(&response)?;
                let cancelled_count = data.get("success-count")
                    .and_then(|v| v.as_u64())
                    .map(|n| n as u32)
                    .unwrap_or(0);
                let failed_count = data.get("failed-count")
                    .and_then(|v| v.as_u64())
                    .map(|n| n as u32)
                    .unwrap_or(0);

                Ok(CancelAllResponse {
                    cancelled_count,
                    failed_count,
                    details: vec![],
                })
            }

            _ => Err(ExchangeError::NotImplemented(
                format!("{:?} not supported in cancel_all_orders", scope)
            )),
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// BATCH ORDERS (optional trait)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl BatchOrders for HtxConnector {
    async fn place_orders_batch(
        &self,
        _orders: Vec<crate::core::OrderRequest>,
    ) -> ExchangeResult<Vec<OrderResult>> {
        // HTX doesn't have a true batch place endpoint for spot
        // Return NotImplemented
        Err(ExchangeError::NotImplemented(
            "Batch order placement not available on HTX spot".to_string()
        ))
    }

    async fn cancel_orders_batch(
        &self,
        order_ids: Vec<String>,
        _symbol: Option<&str>,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<OrderResult>> {
        // HTX: POST /v1/order/orders/batchcancel with {"order-ids": [...]}
        // Max 50 IDs per call
        let chunks: Vec<Vec<String>> = order_ids.chunks(50)
            .map(|chunk| chunk.to_vec())
            .collect();

        let mut results = Vec::new();

        for chunk in chunks {
            let body = json!({ "order-ids": chunk });
            match self.post(HtxEndpoint::CancelAllOrders, body).await {
                Ok(response) => {
                    // Check for success/failed in response
                    let data = HtxParser::extract_result_v1(&response)?;
                    if let Some(success_arr) = data.get("success").and_then(|v| v.as_array()) {
                        for _id_val in success_arr {
                            results.push(OrderResult {
                                order: None,
                                client_order_id: None,
                                success: true,
                                error: None,
                                error_code: None,
                            });
                        }
                    }
                    if let Some(failed_arr) = data.get("failed").and_then(|v| v.as_array()) {
                        for item in failed_arr {
                            let err_msg = item.get("err-msg")
                                .and_then(|v| v.as_str())
                                .unwrap_or("Cancel failed")
                                .to_string();
                            results.push(OrderResult {
                                order: None,
                                client_order_id: None,
                                success: false,
                                error: Some(err_msg),
                                error_code: None,
                            });
                        }
                    }
                }
                Err(e) => {
                    for _ in &chunk {
                        results.push(OrderResult {
                            order: None,
                            client_order_id: None,
                            success: false,
                            error: Some(e.to_string()),
                            error_code: None,
                        });
                    }
                }
            }
        }

        Ok(results)
    }

    fn max_batch_place_size(&self) -> usize {
        0 // Not supported
    }

    fn max_batch_cancel_size(&self) -> usize {
        50
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// ACCOUNT TRANSFERS (optional trait)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AccountTransfers for HtxConnector {
    /// Transfer between Spot and Futures accounts.
    ///
    /// POST /v1/futures/transfer
    /// Body: { currency, amount, type: "pro-to-futures" | "futures-to-pro" }
    async fn transfer(&self, req: TransferRequest) -> ExchangeResult<TransferResponse> {
        use crate::core::AccountType;

        let transfer_type = match (req.from_account, req.to_account) {
            (AccountType::Spot | AccountType::Margin, AccountType::FuturesCross | AccountType::FuturesIsolated) => {
                "pro-to-futures"
            }
            (AccountType::FuturesCross | AccountType::FuturesIsolated, AccountType::Spot | AccountType::Margin) => {
                "futures-to-pro"
            }
            _ => {
                return Err(ExchangeError::NotImplemented(
                    format!("HTX transfer from {:?} to {:?} not supported", req.from_account, req.to_account)
                ));
            }
        };

        let body = json!({
            "currency": req.asset.to_lowercase(),
            "amount": req.amount,
            "type": transfer_type,
        });

        let response = self.post(HtxEndpoint::Transfer, body).await?;

        // HTX v1: {"status": "ok", "data": <tranId>}
        let tran_id = response.get("data")
            .and_then(|v| v.as_i64())
            .map(|n| n.to_string())
            .unwrap_or_else(|| "unknown".to_string());

        Ok(TransferResponse {
            transfer_id: tran_id,
            status: "Successful".to_string(),
            asset: req.asset,
            amount: req.amount,
            timestamp: Some(crate::core::timestamp_millis() as i64),
        })
    }

    /// Get internal transfer history.
    ///
    /// GET /v2/account/transfer
    async fn get_transfer_history(
        &self,
        filter: TransferHistoryFilter,
    ) -> ExchangeResult<Vec<TransferResponse>> {
        let mut params = HashMap::new();

        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.to_string());
        }

        let response = self.get(HtxEndpoint::TransferHistory, params).await?;

        let data = response.get("data")
            .and_then(|v| v.as_array())
            .cloned()
            .unwrap_or_default();

        let records = data.iter().map(|item| {
            let tran_id = item["transactId"]
                .as_str()
                .map(|s| s.to_string())
                .or_else(|| item["transactId"].as_i64().map(|n| n.to_string()))
                .unwrap_or_else(|| "unknown".to_string());

            let asset = item["currency"].as_str().unwrap_or("").to_string();
            let amount = item["amount"]
                .as_str().and_then(|s| s.parse::<f64>().ok())
                .or_else(|| item["amount"].as_f64())
                .unwrap_or(0.0);
            let status = item["state"].as_str().unwrap_or("Unknown").to_string();
            let timestamp = item["createAt"].as_i64();

            TransferResponse {
                transfer_id: tran_id,
                status,
                asset,
                amount,
                timestamp,
            }
        }).collect();

        Ok(records)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// CUSTODIAL FUNDS (optional trait)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CustodialFunds for HtxConnector {
    /// Get deposit address for an asset.
    ///
    /// GET /v2/account/deposit/address
    async fn get_deposit_address(
        &self,
        asset: &str,
        network: Option<&str>,
    ) -> ExchangeResult<DepositAddress> {
        let mut params = HashMap::new();
        params.insert("currency".to_string(), asset.to_lowercase());

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

        let response = self.get(HtxEndpoint::DepositAddress, params).await?;

        // HTX v2: {"code": 200, "data": [{"currency": "...", "address": "...", "chain": "...", "addressTag": "..."}]}
        let data = response.get("data")
            .and_then(|v| v.as_array())
            .and_then(|arr| arr.first())
            .ok_or_else(|| ExchangeError::Parse("No deposit address data".into()))?;

        let address = data["address"]
            .as_str()
            .ok_or_else(|| ExchangeError::Parse("Missing deposit address".into()))?
            .to_string();

        let tag = data["addressTag"]
            .as_str()
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string());

        let net = data["chain"]
            .as_str()
            .or(network)
            .map(|s| s.to_string());

        Ok(DepositAddress {
            address,
            tag,
            network: net,
            asset: asset.to_uppercase(),
            created_at: None,
        })
    }

    /// Submit a withdrawal request.
    ///
    /// POST /v1/dw/withdraw/api/create
    async fn withdraw(&self, req: WithdrawRequest) -> ExchangeResult<WithdrawResponse> {
        let mut body = json!({
            "address": req.address,
            "amount": req.amount.to_string(),
            "currency": req.asset.to_lowercase(),
        });

        if let Some(chain) = &req.network {
            body["chain"] = json!(chain);
        }
        if let Some(tag) = &req.tag {
            body["addr-tag"] = json!(tag);
        }

        let response = self.post(HtxEndpoint::Withdraw, body).await?;

        // HTX v1: {"status": "ok", "data": <withdraw-id>}
        let withdraw_id = response.get("data")
            .and_then(|v| v.as_i64())
            .map(|n| n.to_string())
            .unwrap_or_else(|| "unknown".to_string());

        Ok(WithdrawResponse {
            withdraw_id,
            status: "Pending".to_string(),
            tx_hash: None,
        })
    }

    /// Get deposit and/or withdrawal history.
    ///
    /// GET /v1/query/deposit-withdraw?type=deposit|withdraw
    async fn get_funds_history(
        &self,
        filter: FundsHistoryFilter,
    ) -> ExchangeResult<Vec<FundsRecord>> {
        let mut records = Vec::new();

        let mut base_params = HashMap::new();
        if let Some(asset) = &filter.asset {
            base_params.insert("currency".to_string(), asset.to_lowercase());
        }
        if let Some(limit) = filter.limit {
            base_params.insert("size".to_string(), limit.to_string());
        }

        if matches!(filter.record_type, FundsRecordType::Deposit | FundsRecordType::Both) {
            let mut params = base_params.clone();
            params.insert("type".to_string(), "deposit".to_string());

            let response = self.get(HtxEndpoint::DepositHistory, params).await?;

            let data = HtxParser::extract_result_v1(&response)
                .ok()
                .and_then(|v| v.as_array().cloned())
                .unwrap_or_default();

            for item in &data {
                let id = item["id"]
                    .as_i64().map(|n| n.to_string())
                    .or_else(|| item["id"].as_str().map(|s| s.to_string()))
                    .unwrap_or_default();
                let asset = item["currency"].as_str().unwrap_or("").to_string();
                let amount = item["amount"].as_f64().unwrap_or(0.0);
                let tx_hash = item["txHash"].as_str().map(|s| s.to_string());
                let network = item["chain"].as_str().map(|s| s.to_string());
                let status = item["state"].as_str().unwrap_or("Unknown").to_string();
                let timestamp = item["updatedAt"].as_i64()
                    .or_else(|| item["createAt"].as_i64())
                    .unwrap_or(0);

                records.push(FundsRecord::Deposit {
                    id,
                    asset,
                    amount,
                    tx_hash,
                    network,
                    status,
                    timestamp,
                });
            }
        }

        if matches!(filter.record_type, FundsRecordType::Withdrawal | FundsRecordType::Both) {
            let mut params = base_params;
            params.insert("type".to_string(), "withdraw".to_string());

            let response = self.get(HtxEndpoint::WithdrawHistory, params).await?;

            let data = HtxParser::extract_result_v1(&response)
                .ok()
                .and_then(|v| v.as_array().cloned())
                .unwrap_or_default();

            for item in &data {
                let id = item["id"]
                    .as_i64().map(|n| n.to_string())
                    .or_else(|| item["id"].as_str().map(|s| s.to_string()))
                    .unwrap_or_default();
                let asset = item["currency"].as_str().unwrap_or("").to_string();
                let amount = item["amount"].as_f64().unwrap_or(0.0);
                let fee = item["fee"].as_f64();
                let address = item["address"].as_str().unwrap_or("").to_string();
                let tag = item["addressTag"].as_str()
                    .filter(|s| !s.is_empty())
                    .map(|s| s.to_string());
                let tx_hash = item["txHash"].as_str().map(|s| s.to_string());
                let network = item["chain"].as_str().map(|s| s.to_string());
                let status = item["state"].as_str().unwrap_or("Unknown").to_string();
                let timestamp = item["updatedAt"].as_i64()
                    .or_else(|| item["createAt"].as_i64())
                    .unwrap_or(0);

                records.push(FundsRecord::Withdrawal {
                    id,
                    asset,
                    amount,
                    fee,
                    address,
                    tag,
                    tx_hash,
                    network,
                    status,
                    timestamp,
                });
            }
        }

        Ok(records)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// SUB ACCOUNTS (optional trait)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl SubAccounts for HtxConnector {
    /// Perform sub-account operations: Create, List, Transfer, GetBalance.
    async fn sub_account_operation(
        &self,
        op: SubAccountOperation,
    ) -> ExchangeResult<SubAccountResult> {
        match op {
            SubAccountOperation::Create { label } => {
                // POST /v2/sub-user/creation
                // Body: {"userList": [{"userName": "..."}]}
                let body = json!({
                    "userList": [{"userName": label.clone()}]
                });

                let response = self.post(HtxEndpoint::SubAccountCreate, body).await?;

                // Response: {"code": 200, "data": [{"uid": 123, "userName": "..."}]}
                let data = response.get("data")
                    .and_then(|v| v.as_array())
                    .and_then(|arr| arr.first());

                let id = data.and_then(|d| d["uid"].as_i64()).map(|n| n.to_string());

                Ok(SubAccountResult {
                    id,
                    name: Some(label),
                    accounts: vec![],
                    transaction_id: None,
                })
            }

            SubAccountOperation::List => {
                // GET /v2/sub-user/user-list
                let response = self.get(HtxEndpoint::SubAccountList, HashMap::new()).await?;

                let data = response.get("data")
                    .and_then(|v| v.as_array())
                    .cloned()
                    .unwrap_or_default();

                let accounts = data.iter().map(|item| {
                    let id = item["uid"]
                        .as_i64().map(|n| n.to_string())
                        .unwrap_or_default();
                    let name = item["userName"].as_str().unwrap_or("").to_string();
                    let status = if item["userState"].as_str() == Some("lock") {
                        "Frozen".to_string()
                    } else {
                        "Normal".to_string()
                    };

                    SubAccount { id, name, status }
                }).collect();

                Ok(SubAccountResult {
                    id: None,
                    name: None,
                    accounts,
                    transaction_id: None,
                })
            }

            SubAccountOperation::Transfer { sub_account_id, asset, amount, to_sub } => {
                // POST /v1/subuser/transfer
                // type: "master-transfer-in" (sub→master) or "master-transfer-out" (master→sub)
                let transfer_type = if to_sub {
                    "master-transfer-out"
                } else {
                    "master-transfer-in"
                };

                let sub_uid: i64 = sub_account_id.parse().map_err(|_| {
                    ExchangeError::InvalidRequest(
                        format!("HTX sub-account id must be numeric UID, got: {}", sub_account_id)
                    )
                })?;

                let body = json!({
                    "sub-uid": sub_uid,
                    "currency": asset.to_lowercase(),
                    "amount": amount.to_string(),
                    "type": transfer_type,
                });

                let response = self.post(HtxEndpoint::SubAccountTransfer, body).await?;

                let tran_id = response.get("data")
                    .and_then(|v| v.as_i64())
                    .map(|n| n.to_string());

                Ok(SubAccountResult {
                    id: None,
                    name: None,
                    accounts: vec![],
                    transaction_id: tran_id,
                })
            }

            SubAccountOperation::GetBalance { sub_account_id } => {
                // GET /v1/account/accounts/{sub-uid}
                let path = HtxEndpoint::SubAccountBalance.path_with_vars(&[("sub-uid", &sub_account_id)]);

                let real_base = HtxUrls::base_url(self.testnet);
                let auth = self.auth.as_ref()
                    .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
                let query = auth.build_signed_query("GET", "api.huobi.pro", &path, &HashMap::new());

                let url = assemble_rest_url(self.rest_override.as_deref(), real_base, &path, &format!("?{}", query));

                self.rate_limit_wait(1, true).await;
                let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &HashMap::new()).await?;
                self.update_rate_from_headers(&resp_headers);

                // Balance is available but SubAccountResult doesn't carry raw balances.
                let _ = response;

                Ok(SubAccountResult {
                    id: Some(sub_account_id),
                    name: None,
                    accounts: vec![],
                    transaction_id: None,
                })
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// EXTENDED METHODS (not part of core traits)
// ═══════════════════════════════════════════════════════════════════════════════

impl HtxConnector {
    /// Get fills (match results) for a specific order.
    ///
    /// `GET /v1/order/orders/{order-id}/matchresults`
    ///
    /// Returns a list of trade records that filled the given order.
    pub async fn get_order_match_results(&self, order_id: &str) -> ExchangeResult<Value> {
        let path = HtxEndpoint::OrderMatchResults.path_with_vars(&[("order-id", order_id)]);
        let auth = self.auth.as_ref()
            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;
        let query = auth.build_signed_query("GET", "api.huobi.pro", &path, &HashMap::new());
        let real_base = HtxUrls::base_url(self.testnet);
        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, &path, &format!("?{}", query));
        self.rate_limit_wait(1, true).await;
        let (response, resp_headers) = self.http.get_with_response_headers(&url, &HashMap::new(), &HashMap::new()).await?;
        self.update_rate_from_headers(&resp_headers);
        Ok(response)
    }

    /// Get USDT-margined open interest for a contract.
    ///
    /// `GET /linear-swap-api/v1/swap-open-interest?contract_code={symbol}`
    ///
    /// # Parameters
    /// - `contract_code`: Contract code e.g. `BTC-USDT` (optional — omit for all)
    pub async fn get_open_interest(&self, contract_code: Option<&str>) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        if let Some(code) = contract_code {
            params.insert("contract_code".to_string(), code.to_string());
        }
        self.get(HtxEndpoint::OpenInterest, params).await
    }

    /// Get historical funding rates for a USDT-margined contract.
    ///
    /// `GET /linear-swap-api/v3/swap-funding-rate-history`
    ///
    /// # Parameters
    /// - `contract_code`: Contract code e.g. `BTC-USDT`
    /// - `page_index`: Page index (optional, default 1)
    /// - `page_size`: Page size (optional, default 20, max 50)
    pub async fn get_funding_rate_history(
        &self,
        contract_code: &str,
        page_index: Option<u32>,
        page_size: Option<u32>,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code.to_string());
        if let Some(idx) = page_index {
            params.insert("page_index".to_string(), idx.to_string());
        }
        if let Some(size) = page_size {
            params.insert("page_size".to_string(), size.to_string());
        }
        self.get(HtxEndpoint::FundingRateHistory, params).await
    }

    /// Get mark price and index price for a USDT-margined contract.
    ///
    /// `GET /linear-swap-ex/market/index?contract_code={symbol}`
    ///
    /// Returns both the mark price and index price for the given contract.
    pub async fn get_mark_price(&self, contract_code: &str) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code.to_string());
        self.get(HtxEndpoint::MarkPrice, params).await
    }

    /// Get elite trader long/short account ratio for a USDT-margined contract.
    ///
    /// `GET /linear-swap-api/v1/swap_elite_account_ratio`
    ///
    /// Verified: returns `{list: [{buy_ratio, sell_ratio, locked_ratio, ts}], contract_code, ...}`
    ///
    /// # Parameters
    /// - `contract_code`: Contract code e.g. `BTC-USDT`
    /// - `period`: Period e.g. `5min`, `15min`, `30min`, `60min`, `4hour`, `1day`
    pub async fn get_elite_account_ratio(
        &self,
        contract_code: &str,
        period: &str,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code.to_string());
        params.insert("period".to_string(), period.to_string());
        self.get(HtxEndpoint::EliteAccountRatio, params).await
    }

    /// Get historical funding rates (actual paid rates) for a USDT-margined contract.
    ///
    /// `GET /linear-swap-api/v1/swap_historical_funding_rate`
    ///
    /// Verified: returns `{data: {data: [{avg_premium_index, funding_rate, funding_time, ...}]}}`.
    ///
    /// # Parameters
    /// - `contract_code`: Contract code e.g. `BTC-USDT`
    /// - `page_index`: Page index (optional, default 1)
    /// - `page_size`: Page size (optional, default 20, max 50)
    pub async fn get_historical_funding_rate(
        &self,
        contract_code: &str,
        page_index: Option<u32>,
        page_size: Option<u32>,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code.to_string());
        if let Some(idx) = page_index {
            params.insert("page_index".to_string(), idx.to_string());
        }
        if let Some(size) = page_size {
            params.insert("page_size".to_string(), size.to_string());
        }
        self.get(HtxEndpoint::HistoricalFundingRate, params).await
    }

    /// Get mark price kline (candlestick) data for a USDT-margined contract.
    ///
    /// `GET /linear-swap-ex/market/history/mark_price_kline`
    ///
    /// # Parameters
    /// - `contract_code`: Contract code e.g. `BTC-USDT`
    /// - `period`: Kline period e.g. `1min`, `5min`, `60min`, `1day`
    /// - `size`: Number of bars (optional, default 150, max 2000)
    pub async fn get_mark_price_kline(
        &self,
        contract_code: &str,
        period: &str,
        size: Option<u32>,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code.to_string());
        params.insert("period".to_string(), period.to_string());
        if let Some(s) = size {
            params.insert("size".to_string(), s.to_string());
        }
        self.get(HtxEndpoint::MarkPriceKline, params).await
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// MARKET DATA PUBLIC (REST-historical non-OHLCV)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl crate::core::traits::MarketDataPublic for HtxConnector {
    /// Recent public trades.
    ///
    /// Spot: `GET /market/history/trade?symbol=btcusdt&size=N` (max 2000).
    /// Futures: `GET /linear-swap-ex/market/history/trade?contract_code=BTC-USDT&size=N`.
    ///
    /// Both endpoints return a double-nested shape:
    /// `data[].data[]` — outer array groups micro-batches, inner holds individual fills.
    /// `HtxParser::parse_recent_trades` flattens both layers.
    async fn get_recent_trades(
        &self,
        symbol: SymbolInput<'_>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<crate::core::types::PublicTrade>> {
        let symbol = symbol.resolve(ExchangeId::HTX, account_type)?;
        let is_futures = matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated);
        let mut params = HashMap::new();
        if is_futures {
            params.insert("contract_code".to_string(), to_linear_swap_code(&symbol));
            if let Some(n) = limit {
                params.insert("size".to_string(), n.min(2000).to_string());
            }
            let resp = self.get(HtxEndpoint::FuturesTrades, params).await?;
            HtxParser::parse_recent_trades(&resp)
        } else {
            params.insert("symbol".to_string(), symbol.to_string());
            if let Some(n) = limit {
                params.insert("size".to_string(), n.min(2000).to_string());
            }
            let resp = self.get(HtxEndpoint::HistoryTrades, params).await?;
            HtxParser::parse_recent_trades(&resp)
        }
    }

    /// Mark price klines.
    ///
    /// `GET /index/market/history/linear_swap_mark_price_kline`
    /// Params: `contract_code`, `period`, `size` (max 2000).
    /// 9 periods: 1min 5min 15min 30min 60min 4hour 1day 1week 1mon.
    async fn get_mark_price_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u32>,
        account_type: AccountType,
        _end_time: Option<i64>,
    ) -> ExchangeResult<Vec<crate::core::types::Kline>> {
        let symbol = symbol.resolve(ExchangeId::HTX, account_type)?;
        let contract_code = to_linear_swap_code(&symbol);
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code);
        params.insert("period".to_string(), map_kline_interval(interval).to_string());
        if let Some(sz) = limit {
            params.insert("size".to_string(), sz.min(2000).to_string());
        }
        let resp = self.get(HtxEndpoint::MarkPriceKlineHistory, params).await?;
        HtxParser::parse_derived_klines(&resp)
    }

    /// Index price klines — NOT SUPPORTED on HTX linear swap.
    ///
    /// HTX exposes mark-price, premium-index and estimated-rate klines under
    /// `/index/market/history/` but has no `linear_swap_index_price_kline`
    /// endpoint (verified live: 404). Use index price via the WS `ws_index`
    /// feed or derive from mark/premium.
    async fn get_index_price_klines(
        &self,
        _symbol: SymbolInput<'_>,
        _interval: &str,
        _limit: Option<u32>,
        _account_type: AccountType,
        _end_time: Option<i64>,
    ) -> ExchangeResult<Vec<crate::core::types::Kline>> {
        Err(ExchangeError::WireAbsent(
            "HTX linear swap has no index-price kline REST endpoint (only mark / \
             premium-index / estimated-rate klines exist); use WS ws_index feed".to_string()
        ))
    }

    /// Premium index klines.
    ///
    /// `GET /index/market/history/linear_swap_premium_index_kline`
    /// Params: `contract_code`, `period`, `size` (max 2000). OHLCV of the
    /// premium index (mark vs index basis) — HTX's true premium-index series.
    async fn get_premium_index_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u32>,
        account_type: AccountType,
        _end_time: Option<i64>,
    ) -> ExchangeResult<Vec<crate::core::types::Kline>> {
        let symbol = symbol.resolve(ExchangeId::HTX, account_type)?;
        let contract_code = to_linear_swap_code(&symbol);
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code);
        params.insert("period".to_string(), map_kline_interval(interval).to_string());
        if let Some(sz) = limit {
            params.insert("size".to_string(), sz.min(2000).to_string());
        }
        let resp = self.get(HtxEndpoint::PremiumIndexKlineHistory, params).await?;
        HtxParser::parse_derived_klines(&resp)
    }

    /// Historical open interest.
    ///
    /// `GET /linear-swap-api/v1/swap_his_open_interest`
    /// Params: `contract_code`, `period`, `size` (max 200), `amount_type` (1=cont, 2=crypto).
    /// Note: max 200 pts per call — much smaller than the kline endpoints (2000).
    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<crate::core::types::OpenInterest>> {
        let symbol = symbol.resolve(ExchangeId::HTX, account_type)?;
        let contract_code = to_linear_swap_code(&symbol);
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code);
        params.insert("period".to_string(), map_kline_interval(period).to_string());
        // amount_type 1 = contracts (default)
        params.insert("amount_type".to_string(), "1".to_string());
        if let Some(sz) = limit {
            params.insert("size".to_string(), sz.min(200).to_string());
        }
        let resp = self.get(HtxEndpoint::OpenInterestHistory, params).await?;
        HtxParser::parse_open_interest_history(&resp)
    }

    /// Historical long/short ratio — **elite (top-trader) accounts only**.
    ///
    /// `GET /linear-swap-api/v1/swap_elite_account_ratio`
    /// Params: `contract_code`, `period` (5min buckets confirmed via Tardis).
    ///
    /// # Important
    /// HTX exposes **only elite/top-trader** variants for linear swap (`swap_elite_account_ratio`
    /// + `swap_elite_position_ratio`).  There is **no global account ratio** endpoint for
    /// USDT-margined perpetuals — the global variant is absent from the official docs and
    /// Tardis channel inventory.  `ratio_type` is set to `"eliteAccountRatio"` so callers can
    /// distinguish this from a global sentiment index.
    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<crate::core::types::LongShortRatio>> {
        let symbol = symbol.resolve(ExchangeId::HTX, account_type)?;
        let contract_code = to_linear_swap_code(&symbol);
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code.clone());
        // HTX period strings for elite ratio: 5min 15min 30min 60min 4hour 1day
        params.insert("period".to_string(), map_kline_interval(period).to_string());
        let resp = self.get(HtxEndpoint::EliteAccountRatio, params).await?;
        HtxParser::parse_long_short_ratio(&resp, &contract_code, "eliteAccountRatio")
    }

    /// Historical funding rates (actual settled rates).
    ///
    /// `GET /linear-swap-api/v1/swap_historical_funding_rate`
    /// Paginated via `page_index` / `page_size` (max 50 per page).
    /// The `start_time`, `end_time`, and `limit` params are approximated:
    /// limit drives `page_size` (capped at 50); time filters are not natively
    /// supported by this endpoint (page-based only) — first page is returned.
    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<crate::core::types::FundingRate>> {
        let symbol = symbol.resolve(ExchangeId::HTX, account_type)?;
        let contract_code = to_linear_swap_code(&symbol);
        let page_size = limit.map(|l| l.min(50)).unwrap_or(20);
        let resp = self.get_historical_funding_rate(&contract_code, Some(1), Some(page_size)).await?;
        HtxParser::parse_funding_rate_history(&resp)
    }

    /// Historical basis (futures − index price) time series.
    ///
    /// `GET /linear-swap-ex/market/history/linear_swap_basis`
    /// Params: `contract_code`, `period`, `size` (max 2000), `basis_price_type=close`.
    /// `id` field in each record is seconds → multiplied to ms by the parser.
    async fn get_basis_history(
        &self,
        symbol: SymbolInput<'_>,
        period: &str,
        _start_time: Option<i64>,
        _end_time: Option<i64>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<crate::core::types::Basis>> {
        let symbol = symbol.resolve(ExchangeId::HTX, account_type)?;
        let contract_code = to_linear_swap_code(&symbol);
        let mut params = HashMap::new();
        params.insert("contract_code".to_string(), contract_code);
        params.insert("period".to_string(), map_kline_interval(period).to_string());
        // basis_price_type: open/close/high/low/average — use close (settled bar close).
        params.insert("basis_price_type".to_string(), "close".to_string());
        if let Some(sz) = limit {
            params.insert("size".to_string(), sz.min(2000).to_string());
        }
        let resp = self.get(HtxEndpoint::BasisHistory, params).await?;
        HtxParser::parse_basis_history(&resp)
    }
}

impl crate::core::traits::HasCapabilities for HtxConnector {
    fn capabilities(&self) -> crate::core::types::ConnectorCapabilities {
        crate::core::types::ConnectorCapabilities {
            has_ticker: true, has_orderbook: true, has_klines: true,
            has_recent_trades: true, has_exchange_info: true,
            // MarketDataPublic — all 6 REST-historical methods implemented.
            // has_premium_index is false: HTX has no snapshot premium-index endpoint;
            // estimated-rate kline is served via has_premium_index_klines instead.
            has_liquidation_history: false, has_open_interest_history: true,
            has_premium_index: false, has_long_short_ratio_history: true,
            has_funding_rate_history: true, has_mark_price_klines: true,
            has_basis_history: true,
            has_taker_volume_history: false,
            has_liquidation_bucket_history: false,
            has_insurance_fund: false,
            has_index_price_klines: false,
            has_premium_index_klines: true,
            has_agg_trades: false,            has_market_order: true, has_limit_order: true,
            has_open_orders: true, has_order_history: true, has_user_trades: true,
            has_positions: true, has_mark_price: true, has_modify_position: true,
            has_closed_pnl: false, has_long_short_ratio: false,
            has_cancel_all: true, has_amend_order: false,
            has_batch_place: false, has_batch_cancel: true,
            max_batch_place_size: 0, max_batch_cancel_size: 50,
            has_balance: true, has_account_info: true, has_fees: true,
            has_transfers: true, has_deposit_withdraw: true, has_sub_accounts: true,
            has_funding_payments: false, has_ledger: false,
            has_websocket: true, has_ws_klines: true, has_ws_trades: true,
            has_ws_orderbook: true, has_ws_ticker: true,
            has_ws_mark_price: false, has_ws_funding_rate: false,
            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::HTX)
    }

    fn trade_history_capabilities(&self) -> crate::core::types::TradeHistoryCapabilities {
        use crate::core::types::TradeHistoryTier;
        // /market/history/trade and /linear-swap-ex/market/history/trade
        // are recent-only (no cursor), single-call max = 2000.
        //
        // kline_backpage=false is a CONFIRMED VENUE CEILING, not a gap left
        // for a future fix: Wave 2 live-probed `/market/history/kline`'s
        // `from`/`to` params on both spot and linear-swap — every call
        // returned the identical most-recent window regardless of the
        // requested range. See the doc comment on `get_klines` for the full
        // probe record. The synthetic-kline deep-seed path can't backpage
        // on this venue.
        crate::core::types::TradeHistoryCapabilities {
            spot: TradeHistoryTier::RecentOnly { max_trades: 2000 },
            futures: TradeHistoryTier::RecentOnly { max_trades: 2000 },
            kline_backpage: false,
        }
    }

    fn kline_interval_capabilities(&self) -> crate::core::types::KlineIntervalCapabilities {
        // Probe 2026-07-08: GET /market/history/kline?period=1sec errors
        // with the venue's own authoritative valid-set:
        // `"...k-line time range should be [1min,3min,5min,15min,30min,
        // 1h,4h,6h,12h,1day,1week,1M,6Hutc,12Hutc,1Dutc,3Dutc,1Wutc,
        // 1Mutc]"`. Same interval set is shared between spot and
        // linear-swap (single `period` enum across HTX's history/kline
        // family) — no seconds tier on either. `*utc` variants omitted
        // (UTC-day-boundary alternates, not separate granularities).
        // Matches the connector's own `map_kline_interval` (endpoints.rs)
        // 1m/5m/15m/30m/1h/4h/1d/1w/1M/1y subset.
        crate::core::types::KlineIntervalCapabilities {
            spot: &["1m", "3m", "5m", "15m", "30m", "1h", "4h", "6h", "12h", "1d", "1w", "1M"],
            futures: &["1m", "3m", "5m", "15m", "30m", "1h", "4h", "6h", "12h", "1d", "1w", "1M"],
        }
    }
}
