//! # Gemini Connector
//!
//! Реализация всех core трейтов для Gemini.
//!
//! ## Core трейты
//! - `ExchangeIdentity` - идентификация биржи
//! - `MarketData` - рыночные данные
//! - `Trading` - торговые операции
//! - `Account` - информация об аккаунте
//! - `Positions` - futures позиции
//!
//! ## Extended методы
//! Дополнительные Gemini-специфичные методы как методы структуры.

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

use serde_json::{json, Value};

use crate::core::{
    HttpClient, Credentials, assemble_rest_url,
    ExchangeId, AccountType,
    ExchangeError, ExchangeResult,
    Price, Kline, Ticker, OrderBook,
    SymbolInput,
    Order, OrderSide, OrderType, Balance, AccountInfo,
    Position, FundingRate,
    OrderRequest, CancelRequest, CancelScope,
    BalanceQuery, PositionQuery, PositionModification,
    OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
    CancelAllResponse, CancelAll, CustodialFunds,
    DepositAddress, WithdrawResponse, FundsRecord,
};
use crate::core::types::SymbolInfo;
use crate::core::traits::{
    ExchangeIdentity, MarketData, Trading, Account, Positions, MarketDataPublic,
};
use crate::core::types::{PublicTrade, TradeSide};
use crate::core::{MarketDataCapabilities, TradingCapabilities, AccountCapabilities};
use crate::core::types::ConnectorStats;
use crate::core::types::{WithdrawRequest, FundsHistoryFilter, FundsRecordType};
use crate::core::types::{UserTrade, UserTradeFilter};
use crate::core::utils::{RuntimeLimiter, RateLimitMonitor, RateLimitPressure};
use crate::core::types::{RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, EndpointWeight, OrderbookCapabilities, WsBookChannel};
use crate::core::utils::PrecisionCache;

use super::endpoints::{GeminiUrls, GeminiEndpoint, format_symbol, normalize_symbol, map_kline_interval};
use super::auth::GeminiAuth;
use super::parser::GeminiParser;

// ═══════════════════════════════════════════════════════════════════════════════
// RATE LIMIT CAPABILITIES
// ═══════════════════════════════════════════════════════════════════════════════

static GEMINI_POOLS: &[RestLimitPool] = &[
    RestLimitPool {
        name: "public",
        max_budget: 120,
        window_seconds: 60,
        is_weight: false,
        has_server_headers: false,
        server_header: None,
        header_reports_used: false,
    },
    RestLimitPool {
        name: "private",
        max_budget: 600,
        window_seconds: 60,
        is_weight: false,
        has_server_headers: false,
        server_header: None,
        header_reports_used: false,
    },
];

static GEMINI_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
    model: LimitModel::Group,
    rest_pools: GEMINI_POOLS,
    decaying: None,
    endpoint_weights: &[] as &[EndpointWeight],
    ws: WsLimits {
        max_connections: None,
        max_subs_per_conn: None,
        max_msg_per_sec: None,
        max_streams_per_conn: None,
    },
};

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

/// Gemini коннектор
pub struct GeminiConnector {
    /// HTTP клиент
    http: HttpClient,
    /// Аутентификация (None для публичных методов)
    auth: Option<GeminiAuth>,
    /// URL'ы (mainnet/testnet)
    urls: GeminiUrls,
    /// Testnet mode
    testnet: bool,
    /// REST base URL override for proxy / CORS routing on wasm32.
    /// When set, replaces the exchange's native base URL at every REST call site.
    rest_override: Option<String>,
    /// Runtime rate limiter (Group model: public 120/60s + private 600/60s)
    limiter: Arc<Mutex<RuntimeLimiter>>,
    /// Pressure monitor
    monitor: Arc<Mutex<RateLimitMonitor>>,
    /// Per-symbol precision cache for safe price/qty formatting
    precision: PrecisionCache,
}

impl GeminiConnector {
    /// Создать новый коннектор
    pub async fn new(credentials: Option<Credentials>, testnet: bool) -> ExchangeResult<Self> {
        Self::new_with_override(credentials, testnet, None).await
    }

    /// Create connector with optional REST base URL override.
    ///
    /// When `rest_override` is `Some(url)`, all REST requests use that URL as
    /// the base instead of the exchange's native endpoint. Intended for proxy
    /// and CORS routing on wasm32 (e.g. `ExchangeHub::set_rest_base_override`).
    pub async fn new_with_override(credentials: Option<Credentials>, testnet: bool, rest_override: Option<String>) -> ExchangeResult<Self> {
        let urls = if testnet {
            GeminiUrls::TESTNET
        } else {
            GeminiUrls::MAINNET
        };

        let http = HttpClient::new(30_000)?; // 30 sec timeout

        let auth = credentials
            .as_ref()
            .map(GeminiAuth::new)
            .transpose()?;

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

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

    /// Создать коннектор только для публичных методов
    pub async fn public(testnet: bool, rest_override: Option<String>) -> ExchangeResult<Self> {
        Self::new_with_override(None, testnet, rest_override).await
    }

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

    /// Wait for rate limit if needed.
    ///
    /// Routes to "public" or "private" group. Non-essential (public) requests are
    /// dropped at >= 90% utilization. Returns `true` if acquired, `false` if dropped.
    async fn rate_limit_wait(&self, is_private: bool) -> bool {
        let group = if is_private { "private" } else { "public" };
        let essential = is_private;
        loop {
            let wait_time = {
                let mut limiter = self.limiter.lock().expect("limiter poisoned");
                let pressure = self.monitor.lock().expect("monitor poisoned").check(&mut limiter);
                if pressure >= RateLimitPressure::Cutoff && !essential {
                    return false;
                }
                if limiter.try_acquire(group, 1) {
                    return true;
                }
                limiter.time_until_ready(group, 1)
            };
            if wait_time > Duration::ZERO {
                tokio::time::sleep(wait_time).await;
            }
        }
    }

    /// GET запрос
    async fn get(
        &self,
        endpoint: GeminiEndpoint,
        path_params: &[(&str, &str)],
    ) -> ExchangeResult<Value> {
        // Public GET = non-essential; private GET = essential
        if !self.rate_limit_wait(endpoint.requires_auth()).await {
            return Err(ExchangeError::RateLimitExceeded {
                retry_after: None,
                message: "Rate limit budget >= 90% used; market data request dropped".to_string(),
            });
        }

        let real_base = self.urls.rest_url(AccountType::Spot);
        let mut path = endpoint.path().to_string();

        // Replace path parameters
        for (key, value) in path_params {
            path = path.replace(&format!("{{{}}}", key), value);
        }

        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, &path, "");

        let response = self.http.get(&url, &HashMap::new()).await?;
        GeminiParser::check_error(&response)?;
        Ok(response)
    }

    /// GET запрос with query parameters
    async fn get_with_query(
        &self,
        endpoint: GeminiEndpoint,
        path_params: &[(&str, &str)],
        query: &[(&str, &str)],
    ) -> ExchangeResult<Value> {
        if !self.rate_limit_wait(endpoint.requires_auth()).await {
            return Err(ExchangeError::RateLimitExceeded {
                retry_after: None,
                message: "Rate limit budget >= 90% used; market data request dropped".to_string(),
            });
        }

        let real_base = self.urls.rest_url(AccountType::Spot);
        let mut path = endpoint.path().to_string();

        for (key, value) in path_params {
            path = path.replace(&format!("{{{}}}", key), value);
        }

        let query_str: Vec<String> = query.iter()
            .map(|(k, v)| format!("{}={}", k, v))
            .collect();
        let query_pfx = if query_str.is_empty() { String::new() } else { format!("?{}", query_str.join("&")) };
        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, &path, &query_pfx);

        let response = self.http.get(&url, &HashMap::new()).await?;
        GeminiParser::check_error(&response)?;
        Ok(response)
    }

    /// POST запрос (всегда требует auth)
    async fn post(
        &self,
        endpoint: GeminiEndpoint,
        params: HashMap<String, Value>,
        path_params: &[(&str, &str)],
    ) -> ExchangeResult<Value> {
        // POST is always private + essential
        self.rate_limit_wait(true).await;

        let real_base = self.urls.rest_url(AccountType::Spot);
        let mut path = endpoint.path().to_string();

        // Replace path parameters
        for (key, value) in path_params {
            path = path.replace(&format!("{{{}}}", key), value);
        }

        let url = assemble_rest_url(self.rest_override.as_deref(), real_base, &path, "");

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

        // Gemini POST requests have empty body, everything in headers
        let response = self.http.post(&url, &json!({}), &headers).await?;
        GeminiParser::check_error(&response)?;
        Ok(response)
    }
}

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

impl ExchangeIdentity for GeminiConnector {
    fn exchange_id(&self) -> ExchangeId {
        ExchangeId::Gemini
    }

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

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

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

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

    fn orderbook_capabilities(&self, _account_type: AccountType) -> OrderbookCapabilities {
        static GEMINI_CHANNELS: &[WsBookChannel] = &[
            WsBookChannel::snapshot("depth5",         5,  1000),
            WsBookChannel::snapshot("depth10",        10, 1000),
            WsBookChannel::snapshot("depth20",        20, 1000),
            WsBookChannel::snapshot("depth5@100ms",   5,  100),
            WsBookChannel::snapshot("depth10@100ms",  10, 100),
            WsBookChannel::snapshot("depth20@100ms",  20, 100),
            WsBookChannel::delta("depth",             None, Some(1000)),
            WsBookChannel::delta("depth@100ms",       None, Some(100)),
        ];
        OrderbookCapabilities {
            ws_depths: &[5, 10, 20],
            ws_default_depth: Some(20),
            rest_max_depth: None,
            rest_depth_values: &[],
            supports_snapshot: true,
            supports_delta: true,
            update_speeds_ms: &[100, 1000],
            default_speed_ms: Some(1000),
            ws_channels: GEMINI_CHANNELS,
            checksum: None,
            has_sequence: true,
            has_prev_sequence: false,
            supports_aggregation: false,
            aggregation_levels: &[],
        }
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl MarketData for GeminiConnector {
    async fn get_price(&self, symbol: SymbolInput<'_>, account_type: AccountType) -> ExchangeResult<Price> {
        let symbol = symbol.resolve(ExchangeId::Gemini, account_type)?;
        let symbol_str = normalize_symbol(&symbol);

        let response = self.get(
            GeminiEndpoint::Ticker,
            &[("symbol", &symbol_str)],
        ).await?;

        let ticker = GeminiParser::parse_ticker(&response, &symbol_str)?;
        Ok(ticker.last_price)
    }

    async fn get_ticker(&self, symbol: SymbolInput<'_>, account_type: AccountType) -> ExchangeResult<Ticker> {
        let symbol = symbol.resolve(ExchangeId::Gemini, account_type)?;
        let symbol_str = normalize_symbol(&symbol);

        // V1 pubticker carries bid/ask/last + volume.{base,quote} but NO open.
        // V2 ticker carries open/high/low/close/bid/ask but NO volume.
        // For full lossless coverage, fetch both in parallel and merge: V1 fills
        // volume_24h / quote_volume_24h / timestamp; V2 fills open_price + 24h
        // price-change percent. Both calls are public + idempotent.
        let params: [(&str, &str); 1] = [("symbol", &symbol_str)];
        let (v1_res, v2_res) = tokio::join!(
            self.get(GeminiEndpoint::Ticker, &params),
            self.get(GeminiEndpoint::TickerV2, &params),
        );

        // V1 is the primary source — its bid/ask/volume keep the existing
        // semantics. If V1 fails, propagate the error.
        let v1_response = v1_res?;
        let mut ticker = GeminiParser::parse_ticker(&v1_response, &symbol_str)?;

        // Merge V2 fields if reachable; V2 failure is non-fatal (we still
        // return a valid V1 ticker, just without open_price).
        if let Ok(v2_response) = v2_res {
            if let Ok(v2_ticker) = GeminiParser::parse_ticker(&v2_response, &symbol_str) {
                if ticker.open_price.is_none() {
                    ticker.open_price = v2_ticker.open_price;
                }
                if ticker.price_change_percent_24h.is_none() {
                    ticker.price_change_percent_24h = v2_ticker.price_change_percent_24h;
                }
                if ticker.high_24h.is_none() { ticker.high_24h = v2_ticker.high_24h; }
                if ticker.low_24h.is_none()  { ticker.low_24h  = v2_ticker.low_24h; }
            }
        }
        Ok(ticker)
    }

    async fn get_orderbook(
        &self,
        symbol: SymbolInput<'_>,
        _depth: Option<u16>,
        account_type: AccountType,
    ) -> ExchangeResult<OrderBook> {
        let symbol = symbol.resolve(ExchangeId::Gemini, account_type)?;
        let symbol_str = normalize_symbol(&symbol);

        let response = self.get(
            GeminiEndpoint::OrderBook,
            &[("symbol", &symbol_str)],
        ).await?;

        GeminiParser::parse_orderbook(&response)
    }

    /// `GET /v2/candles/{symbol}/{time_frame}` — genuinely has NO
    /// pagination of any kind.
    ///
    /// Wave 2 investigation (2026-07-08): this is a pure path-based endpoint
    /// (`{symbol}/{time_frame}`, no query string in the official spec) that
    /// returns a FIXED recent window — live-verified 1440 candles at `1m`
    /// (exactly 24h). Live-probed the undocumented `limit`/`since` query
    /// params anyway (in case Gemini silently supported them) — both were
    /// completely ignored, identical 1440-row response either way. There is
    /// no server-side concept of `limit` or `end_time` on this endpoint at
    /// all — not a hard-reject, not a silent clamp, just no such parameter
    /// exists on the wire. `_limit` and `_end_time` are left unused
    /// (documented, not silently dropped).
    /// `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::Gemini, account_type)?;
        let symbol_str = normalize_symbol(&symbol);
        let time_frame = map_kline_interval(interval);

        // Use DerivativeCandles endpoint for futures
        let endpoint = if matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated) {
            GeminiEndpoint::DerivativeCandles
        } else {
            GeminiEndpoint::Candles
        };

        let response = self.get(
            endpoint,
            &[("symbol", &symbol_str), ("time_frame", time_frame)],
        ).await?;

        GeminiParser::parse_candles(&response)
    }

    async fn ping(&self) -> ExchangeResult<()> {
        // Gemini doesn't have a dedicated ping endpoint, use symbols as health check
        self.get(GeminiEndpoint::Symbols, &[]).await?;
        Ok(())
    }

    async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
        // Fetch symbol list first, then get details for each.
        // Gemini has no bulk details endpoint; we call /v1/symbols/details per symbol.
        // Cap at 30 symbols to stay within the e2e_smoke 10-second timeout — enough
        // to validate symbol data quality without fetching the full ~370-symbol universe.
        const MAX_SYMBOLS: usize = 30;

        let symbols_response = self.get(GeminiEndpoint::Symbols, &[]).await?;
        let symbols = GeminiParser::parse_symbols(&symbols_response)?;

        let mut result = Vec::with_capacity(MAX_SYMBOLS);

        for symbol_lower in symbols.iter().filter(|s| s.chars().all(|c| c.is_alphabetic())).take(MAX_SYMBOLS) {
            match self.get(GeminiEndpoint::SymbolDetails, &[("symbol", symbol_lower)]).await {
                Ok(details) => {
                    if let Some(info) = GeminiParser::parse_symbol_details(&details, symbol_lower, account_type) {
                        result.push(info);
                    }
                }
                Err(_) => continue, // Skip symbols where details fetch fails
            }
        }

        self.precision.load_from_symbols(&result);
        Ok(result)
    }

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

        MarketDataCapabilities {
            // Gemini has no dedicated ping endpoint; we use /v1/symbols as health check
            has_ping: true,
            // Both spot and perpetuals work with /v1/pubticker/{symbol}
            has_price: true,
            // Both work with /v2/ticker/{symbol}; TickerV2 accepts perp symbols
            has_ticker: true,
            has_orderbook: true,
            // Both work: spot uses /v2/candles, futures uses /v2/derivatives/candles
            has_klines: true,
            // /v1/symbols + /v1/symbols/details is spot-oriented; perpetuals not listed there
            has_exchange_info: !is_futures,
            // GeminiEndpoint::Trades exists but get_recent_trades is not implemented
            // on the MarketData trait — the inherent endpoint is unused here
            has_recent_trades: false,
            // Both Candles and DerivativeCandles support the same time frames
            supported_intervals: &["1m", "5m", "15m", "30m", "1h", "6h", "1d"],
            // Gemini returns all available candles per time-frame; no limit param in either API
            max_kline_limit: None,
            // l2_updates channel carries orderbook deltas; ticker is approximated via l2 bid/ask.
            has_ws_ticker: true,
            has_ws_trades: true,
            // l2 channel provides full L2 orderbook stream.
            has_ws_orderbook: true,
            // candles_{interval} channel provides kline updates.
            has_ws_klines: true,
        }
    }
}

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

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

        match req.order_type {
            OrderType::Market => {
                let mut params = HashMap::new();
                params.insert("symbol".to_string(), json!(symbol_str));
                params.insert("amount".to_string(), json!(self.precision.qty(&symbol_str, quantity)));
                params.insert("side".to_string(), json!(match side {
                    OrderSide::Buy => "buy",
                    OrderSide::Sell => "sell",
                }));
                params.insert("type".to_string(), json!("exchange market"));

                let response = self.post(GeminiEndpoint::NewOrder, params, &[]).await?;
                GeminiParser::parse_order(&response).map(PlaceOrderResponse::Simple)
            }
            OrderType::Limit { price } => {
                let mut params = HashMap::new();
                params.insert("symbol".to_string(), json!(symbol_str));
                params.insert("amount".to_string(), json!(self.precision.qty(&symbol_str, quantity)));
                params.insert("price".to_string(), json!(self.precision.price(&symbol_str, price)));
                params.insert("side".to_string(), json!(match side {
                    OrderSide::Buy => "buy",
                    OrderSide::Sell => "sell",
                }));
                params.insert("type".to_string(), json!("exchange limit"));

                let response = self.post(GeminiEndpoint::NewOrder, params, &[]).await?;
                GeminiParser::parse_order(&response).map(PlaceOrderResponse::Simple)
            }
            OrderType::StopLimit { stop_price, limit_price } => {
                // Gemini: type="exchange stop limit", stop_price=trigger, price=limit
                let mut params = HashMap::new();
                params.insert("symbol".to_string(), json!(symbol_str));
                params.insert("amount".to_string(), json!(self.precision.qty(&symbol_str, quantity)));
                params.insert("price".to_string(), json!(self.precision.price(&symbol_str, limit_price)));
                params.insert("stop_price".to_string(), json!(self.precision.price(&symbol_str, stop_price)));
                params.insert("side".to_string(), json!(match side {
                    OrderSide::Buy => "buy",
                    OrderSide::Sell => "sell",
                }));
                params.insert("type".to_string(), json!("exchange stop limit"));

                let response = self.post(GeminiEndpoint::NewOrder, params, &[]).await?;
                GeminiParser::parse_order(&response).map(PlaceOrderResponse::Simple)
            }
            OrderType::PostOnly { price } => {
                // Gemini: type="exchange limit" with options=["maker-or-cancel"]
                let mut params = HashMap::new();
                params.insert("symbol".to_string(), json!(symbol_str));
                params.insert("amount".to_string(), json!(self.precision.qty(&symbol_str, quantity)));
                params.insert("price".to_string(), json!(self.precision.price(&symbol_str, price)));
                params.insert("side".to_string(), json!(match side {
                    OrderSide::Buy => "buy",
                    OrderSide::Sell => "sell",
                }));
                params.insert("type".to_string(), json!("exchange limit"));
                params.insert("options".to_string(), json!(["maker-or-cancel"]));

                let response = self.post(GeminiEndpoint::NewOrder, params, &[]).await?;
                GeminiParser::parse_order(&response).map(PlaceOrderResponse::Simple)
            }
            OrderType::Ioc { price } => {
                // Gemini: type="exchange limit" with options=["immediate-or-cancel"]
                let limit_price = price.unwrap_or(0.0);
                let mut params = HashMap::new();
                params.insert("symbol".to_string(), json!(symbol_str));
                params.insert("amount".to_string(), json!(self.precision.qty(&symbol_str, quantity)));
                params.insert("price".to_string(), json!(self.precision.price(&symbol_str, limit_price)));
                params.insert("side".to_string(), json!(match side {
                    OrderSide::Buy => "buy",
                    OrderSide::Sell => "sell",
                }));
                params.insert("type".to_string(), json!("exchange limit"));
                params.insert("options".to_string(), json!(["immediate-or-cancel"]));

                let response = self.post(GeminiEndpoint::NewOrder, params, &[]).await?;
                GeminiParser::parse_order(&response).map(PlaceOrderResponse::Simple)
            }
            OrderType::Fok { price } => {
                // Gemini: type="exchange limit" with options=["fill-or-kill"]
                let mut params = HashMap::new();
                params.insert("symbol".to_string(), json!(symbol_str));
                params.insert("amount".to_string(), json!(self.precision.qty(&symbol_str, quantity)));
                params.insert("price".to_string(), json!(self.precision.price(&symbol_str, price)));
                params.insert("side".to_string(), json!(match side {
                    OrderSide::Buy => "buy",
                    OrderSide::Sell => "sell",
                }));
                params.insert("type".to_string(), json!("exchange limit"));
                params.insert("options".to_string(), json!(["fill-or-kill"]));

                let response = self.post(GeminiEndpoint::NewOrder, params, &[]).await?;
                GeminiParser::parse_order(&response).map(PlaceOrderResponse::Simple)
            }
            _ => Err(ExchangeError::NotImplemented(
                format!("{:?} order type not supported on {:?}", req.order_type, self.exchange_id())
            )),
        }
    }

    async fn get_order_history(
        &self,
        filter: OrderHistoryFilter,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<Order>> {
        // Gemini uses /v1/mytrades (PastTrades) for trade history
        let mut params = HashMap::new();

        // Add symbol filter if provided
        if let Some(ref symbol) = filter.symbol {
            let symbol_str = normalize_symbol(&format_symbol(&symbol.base, &symbol.quote, account_type));
            params.insert("symbol".to_string(), json!(symbol_str));
        }

        // Limit trades returned (max 500 per Gemini docs)
        let limit = filter.limit.unwrap_or(50).min(500);
        params.insert("limit_trades".to_string(), json!(limit));

        // Timestamp filter
        if let Some(since) = filter.start_time {
            params.insert("timestamp".to_string(), json!(since / 1000)); // convert ms to sec
        }

        let response = self.post(GeminiEndpoint::PastTrades, params, &[]).await?;
        GeminiParser::parse_past_trades(&response)
    }

    async fn cancel_order(&self, req: CancelRequest) -> ExchangeResult<Order> {
        match req.scope {
            CancelScope::Single { ref order_id } => {
                let mut params = HashMap::new();
                params.insert("order_id".to_string(), json!(order_id.parse::<i64>().unwrap_or(0)));

                let response = self.post(GeminiEndpoint::CancelOrder, params, &[]).await?;
                GeminiParser::parse_order(&response)
            }
            _ => Err(ExchangeError::NotImplemented(
                format!("{:?} cancel scope not supported on {:?}", req.scope, self.exchange_id())
            )),
        }
    }

    async fn get_order(
        &self,
        _symbol: &str,
        order_id: &str,
        _account_type: AccountType,
    ) -> ExchangeResult<Order> {
        let mut params = HashMap::new();
        params.insert("order_id".to_string(), json!(order_id.parse::<i64>().unwrap_or(0)));

        let response = self.post(GeminiEndpoint::OrderStatus, params, &[]).await?;
        GeminiParser::parse_order(&response)
    }

    async fn get_open_orders(
        &self,
        _symbol: Option<&str>,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<Order>> {
        let response = self.post(GeminiEndpoint::ActiveOrders, HashMap::new(), &[]).await?;
        GeminiParser::parse_orders(&response)
    }

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

        // Symbol is required for Gemini /v1/mytrades.
        // If not provided, we attempt with no symbol (Gemini accepts it for some account types).
        if let Some(ref symbol_str) = filter.symbol {
            let sym_normalized = if symbol_str.contains('/') {
                let parts: Vec<&str> = symbol_str.splitn(2, '/').collect();
                normalize_symbol(&format_symbol(parts[0], parts.get(1).unwrap_or(&"USD"), account_type))
            } else {
                normalize_symbol(symbol_str)
            };
            params.insert("symbol".to_string(), json!(sym_normalized));
        }

        let limit = filter.limit.unwrap_or(50).min(500);
        params.insert("limit_trades".to_string(), json!(limit));

        // Gemini uses Unix timestamp in **seconds** for the `timestamp` param
        if let Some(st) = filter.start_time {
            params.insert("timestamp".to_string(), json!(st / 1000));
        }

        let response = self.post(GeminiEndpoint::PastTrades, params, &[]).await?;
        GeminiParser::parse_user_trades(&response, filter.end_time)
    }

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

        TradingCapabilities {
            has_market_order: true,
            has_limit_order: true,
            // Gemini has no stop-market (trigger-only) order type on either account type
            has_stop_market: false,
            // "exchange stop limit" is a spot-only order type; perpetuals use different flow
            has_stop_limit: !is_futures,
            // No trailing stop on Gemini REST API
            has_trailing_stop: false,
            // No bracket orders
            has_bracket: false,
            // No OCO on Gemini
            has_oco: false,
            // No order amendment endpoint
            has_amend: false,
            // No batch order placement
            has_batch: false,
            max_batch_size: None,
            // CancelAll trait is implemented via /v1/order/cancel/all
            has_cancel_all: true,
            // get_user_trades uses /v1/mytrades (works for both spot and futures trades)
            has_user_trades: true,
            // get_order_history also uses /v1/mytrades
            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 GeminiConnector {
    async fn get_balance(&self, _query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
        let response = self.post(GeminiEndpoint::Balances, HashMap::new(), &[]).await?;
        GeminiParser::parse_balances(&response)
    }

    async fn get_account_info(&self, _account_type: AccountType) -> ExchangeResult<AccountInfo> {
        // Gemini doesn't have a specific account info endpoint
        Ok(AccountInfo {
            account_type: _account_type,
            can_trade: true,
            can_withdraw: true,
            can_deposit: true,
            maker_commission: 0.0,
            taker_commission: 0.0,
            balances: vec![],
        })
    }

    async fn get_fees(&self, symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
        // Use /v1/notionalvolume which returns API fee tier in basis points
        let response = self.post(GeminiEndpoint::NotionalVolume, HashMap::new(), &[]).await?;
        GeminiParser::parse_notional_volume_fees(&response, symbol)
    }

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

        AccountCapabilities {
            // get_balance uses /v1/balances (works for both)
            has_balances: true,
            // get_account_info is a hardcoded stub with no real API call — reports false.
            has_account_info: false,
            // get_fees uses /v1/notionalvolume (spot fee tiers; less meaningful for futures)
            has_fees: !is_futures,
            // No AccountTransfers trait implemented — no internal transfer endpoint
            has_transfers: false,
            // No sub-account management
            has_sub_accounts: false,
            // Deposit/withdraw is a spot/custody concept; perpetuals are GUSD-settled internally
            has_deposit_withdraw: !is_futures,
            // MarginAccount exists as an extended method but no Account-level margin trait
            has_margin: false,
            // StakingBalances endpoint exists but no earn/staking Account trait is implemented
            has_earn_staking: false,
            // /v1/perpetuals/fundingPayment exists and is exposed as get_funding_payments() extended method
            has_funding_history: is_futures,
            // No ledger / transactions trait implemented
            has_ledger: false,
            // No convert/swap support
            has_convert: false,
            // Positions trait is implemented; relevant for futures account types.
            has_positions: is_futures,
        }
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl Positions for GeminiConnector {
    async fn get_positions(&self, _query: PositionQuery) -> ExchangeResult<Vec<Position>> {
        let response = self.post(GeminiEndpoint::Positions, HashMap::new(), &[]).await?;
        GeminiParser::parse_positions(&response)
    }

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

        let symbol_str = normalize_symbol(&format_symbol(&sym.base, &sym.quote, AccountType::FuturesCross));

        let response = self.get(
            GeminiEndpoint::FundingAmount,
            &[("symbol", &symbol_str)],
        ).await?;

        GeminiParser::parse_funding_rate(&response)
    }

    async fn modify_position(&self, req: PositionModification) -> ExchangeResult<()> {
        match req {
            PositionModification::SetLeverage { .. } => {
                // Gemini doesn't have a set leverage endpoint
                Err(ExchangeError::WireAbsent("Set leverage not supported by Gemini".to_string()))
            }
            _ => Err(ExchangeError::NotImplemented(
                format!("{:?} not supported on {:?}", req, self.exchange_id())
            )),
        }
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CancelAll for GeminiConnector {
    async fn cancel_all_orders(
        &self,
        _scope: CancelScope,
        _account_type: AccountType,
    ) -> ExchangeResult<CancelAllResponse> {
        // Gemini /v1/order/cancel/all cancels all session orders globally.
        // There is no per-symbol cancel-all in the REST API.
        let response = self.post(GeminiEndpoint::CancelAllOrders, HashMap::new(), &[]).await?;

        // Response: {"result":"ok","details":{"cancelledOrders":[...],"cancelRejects":[...]}}
        let cancelled_count = response
            .get("details")
            .and_then(|d| d.get("cancelledOrders"))
            .and_then(|arr| arr.as_array())
            .map(|arr| arr.len() as u32)
            .unwrap_or(0);

        let failed_count = response
            .get("details")
            .and_then(|d| d.get("cancelRejects"))
            .and_then(|arr| arr.as_array())
            .map(|arr| arr.len() as u32)
            .unwrap_or(0);

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

// ═══════════════════════════════════════════════════════════════════════════════
// 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 GeminiConnector {
    /// Get a new deposit address for an asset.
    ///
    /// Endpoint: POST /v1/deposit/{currency}/newAddress
    /// The `network` parameter is used as the currency path segment if provided.
    async fn get_deposit_address(
        &self,
        asset: &str,
        network: Option<&str>,
    ) -> ExchangeResult<DepositAddress> {
        let currency = network.unwrap_or(asset).to_lowercase();
        let params = HashMap::new();

        let response = self.post(
            GeminiEndpoint::NewDepositAddress,
            params,
            &[("network", &currency)],
        ).await?;

        // Response: {"currency": "BTC", "address": "...", "label": "...", "timestamp": ...}
        let address = response.get("address")
            .and_then(|v| v.as_str())
            .ok_or_else(|| ExchangeError::Parse("Missing address in deposit address response".to_string()))?
            .to_string();

        let created_at = response.get("timestamp")
            .and_then(|v| v.as_i64());

        Ok(DepositAddress {
            address,
            tag: None, // Gemini doesn't return a tag/memo for standard addresses
            network: Some(currency),
            asset: asset.to_string(),
            created_at,
        })
    }

    /// Submit a withdrawal request.
    ///
    /// Endpoint: POST /v1/withdraw/{currency}
    /// Params: address, amount
    async fn withdraw(&self, req: WithdrawRequest) -> ExchangeResult<WithdrawResponse> {
        let currency = req.asset.to_lowercase();

        let mut params = HashMap::new();
        params.insert("address".to_string(), json!(req.address));
        params.insert("amount".to_string(), json!(req.amount.to_string()));

        let response = self.post(
            GeminiEndpoint::Withdraw,
            params,
            &[("currency", &currency)],
        ).await?;

        // Response: {"destination": "...", "amount": "...", "txHash": "...", "withdrawalId": "..."}
        // or on error: {"result": "error", "reason": "..."}
        let withdraw_id = response.get("withdrawalId")
            .or_else(|| response.get("id"))
            .and_then(|v| v.as_str())
            .unwrap_or("")
            .to_string();

        let tx_hash = response.get("txHash")
            .and_then(|v| v.as_str())
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string());

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

    /// Get deposit and/or withdrawal history via the transfers endpoint.
    ///
    /// Endpoint: POST /v1/transfers
    /// Both deposits and withdrawals are returned by the same endpoint.
    /// Filtered client-side by `type` field ("Deposit" or "Withdrawal").
    async fn get_funds_history(
        &self,
        filter: FundsHistoryFilter,
    ) -> ExchangeResult<Vec<FundsRecord>> {
        let mut params = HashMap::new();

        if let Some(limit) = filter.limit {
            params.insert("limit_transfers".to_string(), json!(limit.min(50u32)));
        }

        if let Some(start) = filter.start_time {
            // Gemini uses Unix timestamp in seconds
            params.insert("timestamp".to_string(), json!(start / 1000));
        }

        let response = self.post(GeminiEndpoint::Transfers, params, &[]).await?;

        // Response is an array of transfer objects:
        // {"type": "Deposit"|"Withdrawal", "status": "...", "timestampms": ...,
        //  "eid": ..., "currency": "...", "amount": "...",
        //  "destination": "...", "txHash": "...", "feeAmount": "..."}
        let records = if let Some(arr) = response.as_array() {
            arr.iter().filter_map(|item| {
                let obj = item.as_object()?;

                let transfer_type = obj.get("type")?.as_str()?;
                let currency = obj.get("currency")?.as_str().unwrap_or("").to_uppercase();

                // Filter by asset if specified
                if let Some(ref asset_filter) = filter.asset {
                    if !currency.eq_ignore_ascii_case(asset_filter) {
                        return None;
                    }
                }

                let id = obj.get("eid").and_then(|v| v.as_i64()).map(|v| v.to_string())
                    .or_else(|| obj.get("eventId").and_then(|v| v.as_str()).map(|s| s.to_string()))
                    .unwrap_or_default();
                let amount_str = obj.get("amount").and_then(|v| v.as_str()).unwrap_or("0");
                let amount = amount_str.parse::<f64>().unwrap_or(0.0);
                let timestamp = obj.get("timestampms").and_then(|v| v.as_i64()).unwrap_or(0);
                let status = obj.get("status").and_then(|v| v.as_str()).unwrap_or("Unknown").to_string();
                let tx_hash = obj.get("txHash")
                    .and_then(|v| v.as_str())
                    .filter(|s| !s.is_empty())
                    .map(|s| s.to_string());

                match transfer_type {
                    "Deposit" | "deposit" => {
                        if matches!(filter.record_type, FundsRecordType::Deposit | FundsRecordType::Both) {
                            Some(FundsRecord::Deposit {
                                id,
                                asset: currency,
                                amount,
                                tx_hash,
                                network: None,
                                status,
                                timestamp,
                            })
                        } else {
                            None
                        }
                    }
                    "Withdrawal" | "withdrawal" => {
                        if matches!(filter.record_type, FundsRecordType::Withdrawal | FundsRecordType::Both) {
                            let address = obj.get("destination")
                                .and_then(|v| v.as_str())
                                .unwrap_or("")
                                .to_string();
                            let fee_str = obj.get("feeAmount").and_then(|v| v.as_str()).unwrap_or("0");
                            let fee = fee_str.parse::<f64>().ok().filter(|&f| f > 0.0);

                            Some(FundsRecord::Withdrawal {
                                id,
                                asset: currency,
                                amount,
                                fee,
                                address,
                                tag: None,
                                tx_hash,
                                network: None,
                                status,
                                timestamp,
                            })
                        } else {
                            None
                        }
                    }
                    _ => None,
                }
            }).collect()
        } else {
            vec![]
        };

        Ok(records)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// EXTENDED METHODS (Gemini-специфичные)
// ═══════════════════════════════════════════════════════════════════════════════

impl GeminiConnector {
    /// Get all available symbols
    pub async fn get_symbols(&self) -> ExchangeResult<Vec<String>> {
        let response = self.get(GeminiEndpoint::Symbols, &[]).await?;
        GeminiParser::parse_symbols(&response)
    }

    /// Get notional volume and fee information
    pub async fn get_notional_volume(&self) -> ExchangeResult<Value> {
        self.post(GeminiEndpoint::NotionalVolume, HashMap::new(), &[]).await
    }

    /// Get funding payment history for perpetuals
    pub async fn get_funding_payments(
        &self,
        since: Option<i64>,
        to: Option<i64>,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();

        if let Some(s) = since {
            params.insert("since".to_string(), json!(s));
        }
        if let Some(t) = to {
            params.insert("to".to_string(), json!(t));
        }

        self.post(GeminiEndpoint::FundingPayments, params, &[]).await
    }

    /// Get margin account summary
    pub async fn get_margin_info(&self) -> ExchangeResult<Value> {
        self.post(GeminiEndpoint::MarginAccount, HashMap::new(), &[]).await
    }

    /// Fetch recent public trades for a symbol, including break trades.
    ///
    /// Break trades are flagged by Gemini when trades are broken post-execution
    /// (e.g. due to erroneous fills). When `include_breaks=true` the response
    /// may include entries with `"break": "BrokenTrade"`.
    ///
    /// Endpoint: `GET /v1/trades/{symbol}?limit_trades=N&include_breaks=true`
    ///
    /// Verified live: `GET /v1/trades/btcusd?limit_trades=2&include_breaks=true`
    /// returns the same shape as regular trades — each element has `timestamp`,
    /// `timestampms`, `tid`, `price`, `amount`, `exchange`, `type`.
    ///
    /// Returns raw JSON value for caller flexibility (break field presence varies
    /// and Gemini does not guarantee format stability for rarely-triggered fields).
    pub async fn get_trades_with_breaks(
        &self,
        symbol: &str,
        limit: Option<u32>,
        since_tid: Option<i64>,
    ) -> ExchangeResult<Value> {
        let sym_lower = symbol.to_lowercase();
        let limit_str;
        let tid_str;
        let mut query: Vec<(&str, &str)> = vec![("include_breaks", "true")];
        if let Some(l) = limit {
            limit_str = l.to_string();
            query.push(("limit_trades", &limit_str));
        }
        if let Some(tid) = since_tid {
            tid_str = tid.to_string();
            query.push(("since", &tid_str));
        }
        self.get_with_query(
            GeminiEndpoint::Trades,
            &[("symbol", &sym_lower)],
            &query,
        ).await
    }
}

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

    #[tokio::test]
    async fn test_connector_creation() {
        let connector = GeminiConnector::public(false, None).await.unwrap();
        assert_eq!(connector.exchange_id(), ExchangeId::Gemini);
        assert!(!connector.is_testnet());
    }

    #[test]
    fn test_format_symbol() {
        let symbol = format_symbol("BTC", "USD", AccountType::Spot);
        assert_eq!(symbol, "btcusd");

        let symbol = format_symbol("ETH", "USD", AccountType::FuturesCross);
        assert_eq!(symbol, "ethgusdperp");
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl MarketDataPublic for GeminiConnector {
    /// Recent public trades for a symbol.
    ///
    /// `GET /v1/trades/{symbol}?limit_trades=N`
    /// Response: `[{tid,timestamp,price,amount,type}]`
    /// type: "buy"/"sell". timestamp: unix seconds.
    async fn get_recent_trades(
        &self,
        symbol: SymbolInput<'_>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<PublicTrade>> {
        let sym = symbol.resolve(ExchangeId::Gemini, account_type)?;
        let limit_str = limit.unwrap_or(50).to_string();
        let raw = self.get_with_query(
            GeminiEndpoint::Trades,
            &[("symbol", &sym)],
            &[("limit_trades", &limit_str)],
        ).await?;
        let arr = raw.as_array().ok_or_else(|| {
            ExchangeError::Parse("get_recent_trades: expected array".into())
        })?;
        let mut result = Vec::with_capacity(arr.len());
        for item in arr {
            let parse_f64 = |key: &str| -> f64 {
                item.get(key)
                    .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))
                    .unwrap_or(0.0)
            };
            let trade_type = item.get("type").and_then(|v| v.as_str()).unwrap_or("buy");
            let side = if trade_type.eq_ignore_ascii_case("sell") { TradeSide::Sell } else { TradeSide::Buy };
            let ts_secs = item.get("timestamp").and_then(|v| v.as_i64()).unwrap_or(0);
            result.push(PublicTrade {
                id: item.get("tid").and_then(|v| v.as_i64()).map(|id| id.to_string()).unwrap_or_default(),
                price: parse_f64("price"),
                quantity: parse_f64("amount"),
                side,
                timestamp: ts_secs * 1000,
                ..Default::default()
            });
        }
        Ok(result)
    }
}

impl crate::core::traits::HasCapabilities for GeminiConnector {
    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,
            has_liquidation_history: false, has_open_interest_history: false,
            has_premium_index: false, has_long_short_ratio_history: false,
            has_funding_rate_history: false, has_mark_price_klines: false,
            has_basis_history: false,
            has_taker_volume_history: false,
            has_liquidation_bucket_history: false,
            has_insurance_fund: false,
            has_index_price_klines: false,
            has_premium_index_klines: false,
            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: false, has_mark_price: false, has_modify_position: false,
            has_closed_pnl: false, has_long_short_ratio: false,
            has_cancel_all: true, has_amend_order: false,
            has_batch_place: false, has_batch_cancel: false,
            max_batch_place_size: 0, max_batch_cancel_size: 0,
            has_balance: true, has_account_info: true, has_fees: true,
            has_transfers: false, has_deposit_withdraw: true, has_sub_accounts: false,
            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::Gemini)
    }

    fn trade_history_capabilities(&self) -> crate::core::types::TradeHistoryCapabilities {
        use crate::core::types::TradeHistoryTier;
        // GET /v1/trades/{symbol} has a `since_tid` offset but no true
        // backward pagination in our wiring — out of Wave 2's kline-
        // pagination scope, treated conservatively as recent-only. Gemini
        // has no futures/perp market.
        //
        // kline_backpage=false is a CONFIRMED VENUE CEILING, not a gap left
        // for a future fix: Wave 2 live-probed `/v2/candles/{symbol}/
        // {time_frame}` — a pure path-based endpoint with no query string
        // in the spec — and confirmed even the undocumented `limit`/`since`
        // params are silently ignored (identical fixed 1440-row response
        // regardless). See the doc comment on `get_klines` for the full
        // probe record.
        crate::core::types::TradeHistoryCapabilities {
            spot: TradeHistoryTier::RecentOnly { max_trades: 500 },
            futures: TradeHistoryTier::RecentOnly { max_trades: 0 },
            kline_backpage: false,
        }
    }

    fn kline_interval_capabilities(&self) -> crate::core::types::KlineIntervalCapabilities {
        // Probe 2026-07-08: GET /v2/candles/btcusd/1m succeeds; GET
        // /v2/candles/btcusd/1s errors with the venue's own authoritative
        // valid-set: `"time_frame expects one of the following: [1m, 5m,
        // 15m, 30m, 1hr, 6hr, 1day]"`. No seconds tier, no 4h/1w/1M —
        // matches the connector's own `map_kline_interval` (endpoints.rs)
        // exactly. Gemini has no futures/perp market (see WireAbsent
        // above).
        crate::core::types::KlineIntervalCapabilities {
            spot: &["1m", "5m", "15m", "30m", "1h", "6h", "1d"],
            futures: &[],
        }
    }
}
