//! # Bitfinex Connector
//!
//! Implementation of all core traits for Bitfinex API v2.
//!
//! ## Core Traits
//! - `ExchangeIdentity` - Exchange identification
//! - `MarketData` - Market data endpoints
//! - `Trading` - Trading operations
//! - `Account` - Account information
//! - `Positions` - Margin/futures positions

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

use super::endpoints::{BitfinexUrls, BitfinexEndpoint, format_symbol, build_candle_key};
use super::auth::BitfinexAuth;
use super::parser::BitfinexParser;

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

static BITFINEX_RATE_POOLS: &[RestLimitPool] = &[RestLimitPool {
    name: "default",
    max_budget: 90,
    window_seconds: 60,
    is_weight: false,
    has_server_headers: false,
    server_header: None,
    header_reports_used: false,
}];

static BITFINEX_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
    model: LimitModel::Simple,
    rest_pools: BITFINEX_RATE_POOLS,
    decaying: None,
    endpoint_weights: &[],
    ws: WsLimits {
        max_connections: None,
        max_subs_per_conn: Some(30),
        max_msg_per_sec: None,
        max_streams_per_conn: None,
    },
};

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

/// Bitfinex connector
pub struct BitfinexConnector {
    /// HTTP client
    http: HttpClient,
    /// Authentication (None for public methods only)
    auth: Option<BitfinexAuth>,
    /// URLs (mainnet — Bitfinex has no separate testnet URLs)
    urls: BitfinexUrls,
    /// Paper-trading mode flag.
    /// Bitfinex has no dedicated testnet; paper trading uses prefixed symbols
    /// (e.g., tTESTBTC:TESTUSD) on the same mainnet endpoints.
    /// Stored here for future paper trading symbol routing support.
    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 (Simple model: 90 req/60s)
    limiter: Arc<Mutex<RuntimeLimiter>>,
    /// Pressure monitor — logs transitions, gates non-essential requests at >= 90%
    monitor: Arc<Mutex<RateLimitMonitor>>,
    /// Per-symbol precision cache for safe price/qty formatting
    precision: PrecisionCache,
}

impl BitfinexConnector {
    /// Create new connector
    ///
    /// Note: Bitfinex has no separate testnet URLs. When `testnet` is `true`
    /// the connector still connects to the same mainnet endpoints; paper trading
    /// requires using prefixed symbols like `tTESTBTC:TESTUSD`.
    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 = BitfinexUrls::MAINNET;
        let http = HttpClient::new(30_000)?; // 30 sec timeout

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

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

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

    /// Create connector for public methods only
    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 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: BitfinexEndpoint,
        path_params: &[(&str, &str)],
        query_params: HashMap<String, String>,
    ) -> ExchangeResult<Value> {
        if !self.rate_limit_wait(1, false).await {
            return Err(ExchangeError::RateLimitExceeded {
                retry_after: None,
                message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
            });
        }

        let real_base = self.urls.rest_url(endpoint.requires_auth());
        let mut path = endpoint.path().to_string();

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

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

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

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

    /// POST request (authenticated)
    async fn post(
        &self,
        endpoint: BitfinexEndpoint,
        path_params: &[(&str, &str)],
        body: Value,
    ) -> ExchangeResult<Value> {
        self.rate_limit_wait(1, true).await;

        let real_base = self.urls.rest_url(true); // Always use auth URL for POST
        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, "");

        // Get auth headers
        let auth = self.auth.as_ref()
            .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;

        // API path without /v2 prefix (auth expects "v2/auth/r/wallets" not "/v2/auth/r/wallets")
        let api_path = path.trim_start_matches('/');
        let body_str = body.to_string();
        let headers = auth.sign_request(api_path, &body_str);

        let response = self.http.post(&url, &body, &headers).await?;
        BitfinexParser::check_error(&response)?;
        Ok(response)
    }

    /// Format symbol helper
    fn fmt_symbol(symbol: &Symbol, account_type: AccountType) -> String {
        if let Some(raw) = symbol.raw() {
            raw.to_string()
        } else {
            format_symbol(&symbol.base, &symbol.quote, account_type)
        }
    }

    /// Determine if account type is derivatives (margin/futures)
    fn is_derivatives(account_type: AccountType) -> bool {
        matches!(account_type, AccountType::Margin | AccountType::FuturesCross | AccountType::FuturesIsolated)
    }

    /// Get order type string prefix ("EXCHANGE " for spot, "" for margin/futures)
    fn order_type_prefix(account_type: AccountType) -> &'static str {
        if Self::is_derivatives(account_type) {
            ""
        } else {
            "EXCHANGE "
        }
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // DERIVATIVES MARKET DATA (public)
    // ═══════════════════════════════════════════════════════════════════════════

    /// Get historical funding stats for a perpetual symbol.
    ///
    /// `symbol`: Bitfinex funding currency symbol, e.g. `"fUSD"` or `"fBTC"`.
    /// Returns an array of `[MTS, FRR, AVG_PERIOD, FUNDING_AMOUNT, FUNDING_AMOUNT_USED, FUNDING_BELOW_THRESHOLD]`.
    ///
    /// REST endpoint: `GET /v2/funding/stats/{symbol}/hist`
    pub async fn get_funding_stats(
        &self,
        symbol: &str,
        limit: Option<u32>,
        start: Option<i64>,
        end: Option<i64>,
    ) -> ExchangeResult<Value> {
        let mut query = HashMap::new();
        if let Some(l) = limit {
            query.insert("limit".to_string(), l.to_string());
        }
        if let Some(s) = start {
            query.insert("start".to_string(), s.to_string());
        }
        if let Some(e) = end {
            query.insert("end".to_string(), e.to_string());
        }
        self.get(
            BitfinexEndpoint::FundingStats,
            &[("symbol", symbol)],
            query,
        ).await
    }

    /// Historical derivative status snapshots.
    ///
    /// Endpoint: `GET /v2/status/deriv/{symbol}/hist`
    ///
    /// Each element is a positional array matching the layout of the derivative
    /// `status` WebSocket channel:
    /// `[MTS, null, DERIV_PRICE, SPOT_PRICE, null, INSURANCE_FUND_BALANCE, null,
    ///   NEXT_FUNDING_EVT_TIMESTAMP, NEXT_FUNDING_ACCRUED, NEXT_FUNDING_STEP,
    ///   null, CURRENT_FUNDING, null, null, MARK_PRICE, null, null,
    ///   OPEN_INTEREST, …]`
    ///
    /// Verified live: `GET /v2/status/deriv/tBTCF0:USTF0/hist?limit=3`
    /// returns arrays of ~22 elements; indices match the WS `status` channel layout.
    ///
    /// # Parameters
    /// - `symbol` — derivative symbol, e.g. `"tBTCF0:USTF0"`
    /// - `start`  — start timestamp in milliseconds (optional)
    /// - `end`    — end timestamp in milliseconds (optional)
    /// - `limit`  — max snapshots to return, default 25, max 250 (optional)
    /// - `sort`   — 1 = ascending, -1 = descending (optional)
    pub async fn get_derivative_status_history(
        &self,
        symbol: &str,
        start: Option<i64>,
        end: Option<i64>,
        limit: Option<u32>,
        sort: Option<i8>,
    ) -> ExchangeResult<Value> {
        let mut query = HashMap::new();
        if let Some(s) = start {
            query.insert("start".to_string(), s.to_string());
        }
        if let Some(e) = end {
            query.insert("end".to_string(), e.to_string());
        }
        if let Some(l) = limit {
            query.insert("limit".to_string(), l.min(250).to_string());
        }
        if let Some(s) = sort {
            query.insert("sort".to_string(), s.to_string());
        }
        self.get(
            BitfinexEndpoint::DerivativeStatusHist,
            &[("symbol", symbol)],
            query,
        ).await
    }

    /// Position-size history for long/short ratio computation.
    ///
    /// Endpoint: `GET /v2/stats1/pos.size:1m:{key}/{section}`
    ///
    /// where `key` encodes the symbol and side:
    /// - For long positions: `pos.size:1m:{sym}:long`
    /// - For short positions: `pos.size:1m:{sym}:short`
    ///
    /// `sym` is a trading-pair symbol such as `tBTCUSD` (spot) or `tBTCF0:USTF0`
    /// (perp). Note: spot pairs are confirmed; perp pairs are **unverified** —
    /// see `get_long_short_ratio_history` doc comment.
    ///
    /// Returns `[[MTS, VALUE], ...]` arrays (i.e. position size in base units).
    pub async fn get_pos_size_hist(
        &self,
        sym: &str,
        side: &str,   // "long" or "short"
        start: Option<i64>,
        end: Option<i64>,
        limit: Option<u32>,
        sort: Option<i8>,
    ) -> ExchangeResult<serde_json::Value> {
        // Build the stats1 key: pos.size:1m:{sym}:{side}
        let key = format!("pos.size:1m:{}:{}", sym, side);

        let mut query = HashMap::new();
        if let Some(s) = start {
            query.insert("start".to_string(), s.to_string());
        }
        if let Some(e) = end {
            query.insert("end".to_string(), e.to_string());
        }
        if let Some(l) = limit {
            query.insert("limit".to_string(), l.min(10000).to_string());
        }
        if let Some(s) = sort {
            query.insert("sort".to_string(), s.to_string());
        }

        self.get(
            BitfinexEndpoint::Stats1PosSize,
            &[("key", &key)],
            query,
        ).await
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// 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 BitfinexConnector {
    // ── Mark / Index / Premium klines: wire-absent ───────────────────────────
    //
    // Bitfinex has NO dedicated kline (OHLC) endpoints for mark price, index
    // price, or premium index. The `/v2/status/deriv/{key}/hist` endpoint carries
    // MARK_PRICE (idx 15) and SPOT_PRICE (idx 4) as event snapshots at irregular
    // intervals — NOT as bar-aligned OHLC candles. There is no kline section
    // (`trade:{tf}:{sym}` only applies to trade-price candles).
    // Source: https://docs.bitfinex.com/reference/rest-public-derivatives-status-history

    async fn get_mark_price_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> crate::core::ExchangeResult<Vec<crate::core::types::Kline>> {
        let _ = (symbol, interval, limit, account_type, end_time);
        Err(crate::core::ExchangeError::WireAbsent(
            "Bitfinex: no mark-price kline endpoint — MARK_PRICE appears only as irregular \
             event snapshots in /v2/status/deriv/{key}/hist, not as OHLC candles"
                .into(),
        ))
    }

    async fn get_index_price_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> crate::core::ExchangeResult<Vec<crate::core::types::Kline>> {
        let _ = (symbol, interval, limit, account_type, end_time);
        Err(crate::core::ExchangeError::WireAbsent(
            "Bitfinex: no index-price kline endpoint — SPOT_PRICE (index reference) appears \
             only as irregular event snapshots in /v2/status/deriv/{key}/hist, not as OHLC candles"
                .into(),
        ))
    }

    async fn get_premium_index_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> crate::core::ExchangeResult<Vec<crate::core::types::Kline>> {
        let _ = (symbol, interval, limit, account_type, end_time);
        Err(crate::core::ExchangeError::WireAbsent(
            "Bitfinex: no premium-index kline endpoint — CURRENT_FUNDING/NEXT_FUNDING_ACCRUED \
             in /v2/status/deriv/{key}/hist are funding-rate snapshots, not premium-index OHLC"
                .into(),
        ))
    }

    // ── Funding rate history ──────────────────────────────────────────────────
    //
    // Source: GET /v2/status/deriv/{key}/hist — CURRENT_FUNDING at index 12.
    // Event-based snapshots; `start`/`end` in ms; ≤ 5000 per call.
    // Source: https://docs.bitfinex.com/reference/rest-public-derivatives-status-history

    async fn get_funding_rate_history(
        &self,
        symbol: SymbolInput<'_>,
        start_time: Option<i64>,
        end_time: Option<i64>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> crate::core::ExchangeResult<Vec<FundingRate>> {
        let sym = symbol.resolve(crate::core::ExchangeId::Bitfinex, account_type)?;
        let raw = self
            .get_derivative_status_history(
                &sym,
                start_time,
                end_time,
                limit.map(|l| l.min(5000)),
                None,
            )
            .await?;
        BitfinexParser::parse_deriv_funding_rate_history(&raw, &sym)
    }

    // ── Mark price (derivative status snapshot) ───────────────────────────────
    //
    // Source: GET /v2/status/deriv/{key}/hist — MARK_PRICE idx14, SPOT idx3,
    // CURRENT_FUNDING idx11 (hist form). Returns the most-recent row as a MarkPrice.

    async fn get_premium_index(
        &self,
        symbol: Option<SymbolInput<'_>>,
        account_type: AccountType,
    ) -> crate::core::ExchangeResult<Vec<MarkPrice>> {
        let sym = match symbol {
            Some(s) => s.resolve(crate::core::ExchangeId::Bitfinex, account_type)?,
            None => return Err(ExchangeError::NotImplemented(
                "Bitfinex get_premium_index requires a symbol (deriv-status is per-instrument)".into(),
            )),
        };
        let raw = self
            .get_derivative_status_history(&sym, None, None, Some(1), None)
            .await?;
        BitfinexParser::parse_deriv_mark_price_history(&raw, &sym)
    }

    // ── Insurance fund ────────────────────────────────────────────────────────
    //
    // Source: GET /v2/status/deriv/{key}/hist — INSURANCE_FUND_BALANCE at idx5
    // (hist form). Same sparse-array endpoint as funding/mark/OI.

    async fn get_insurance_fund(
        &self,
        symbol: Option<SymbolInput<'_>>,
        account_type: AccountType,
    ) -> crate::core::ExchangeResult<Vec<crate::core::types::InsuranceFund>> {
        let sym = match symbol {
            Some(s) => s.resolve(crate::core::ExchangeId::Bitfinex, account_type)?,
            None => return Err(ExchangeError::NotImplemented(
                "Bitfinex get_insurance_fund requires a symbol (deriv-status is per-instrument)".into(),
            )),
        };
        let raw = self
            .get_derivative_status_history(&sym, None, None, Some(1), None)
            .await?;
        BitfinexParser::parse_deriv_insurance_fund_history(&raw)
    }

    // ── Open interest history ─────────────────────────────────────────────────
    //
    // Source: GET /v2/status/deriv/{key}/hist — OPEN_INTEREST at index 18.
    // Same endpoint as funding rate history; same depth/granularity.

    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,
    ) -> crate::core::ExchangeResult<Vec<OpenInterest>> {
        let sym = symbol.resolve(crate::core::ExchangeId::Bitfinex, account_type)?;
        let raw = self
            .get_derivative_status_history(
                &sym,
                start_time,
                end_time,
                limit.map(|l| l.min(5000)),
                None,
            )
            .await?;
        BitfinexParser::parse_deriv_open_interest_history(&raw)
    }

    // ── Long/short ratio history ──────────────────────────────────────────────
    //
    // Source: GET /v2/stats1/pos.size:1m:{sym}:{side}/hist
    // Returns POSITION SIZES (not a pre-computed ratio). This method fetches
    // both long and short size series and computes the ratio in-process.
    //
    // NOTE: `period` is ignored — Bitfinex stats1 pos.size is fixed at 1-minute
    // granularity. `limit` caps at 10 000.
    //
    // IMPORTANT — perp-pair uncertainty: the Bitfinex docs confirm this endpoint
    // for spot pairs (e.g. `tBTCUSD`). Support for perpetual pairs
    // (`tBTCF0:USTF0`) is **unverified** — the endpoint may return empty arrays
    // or an error for perp symbols. Callers should handle empty results gracefully.
    // Source: https://docs.bitfinex.com/reference/rest-public-stats

    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,
    ) -> crate::core::ExchangeResult<Vec<LongShortRatio>> {
        let sym = symbol.resolve(crate::core::ExchangeId::Bitfinex, account_type)?;
        let cap = limit.map(|l| l.min(10000));

        // Fetch long-side position sizes
        let long_raw = self
            .get_pos_size_hist(&sym, "long", start_time, end_time, cap, None)
            .await?;
        let long_series = BitfinexParser::parse_pos_size_hist(&long_raw)?;

        // Fetch short-side position sizes
        let short_raw = self
            .get_pos_size_hist(&sym, "short", start_time, end_time, cap, None)
            .await?;
        let short_series = BitfinexParser::parse_pos_size_hist(&short_raw)?;

        // Join on timestamp and compute ratio.
        // Build a map from the shorter series for O(n log n) join.
        use std::collections::BTreeMap;
        let short_map: BTreeMap<i64, f64> = short_series.into_iter().collect();

        let mut out = Vec::with_capacity(long_series.len());
        for (ts, long_size) in long_series {
            let short_size = match short_map.get(&ts) {
                Some(&s) => s,
                None => continue,
            };
            let total = long_size + short_size;
            let (long_ratio, short_ratio, ratio) = if total > 0.0 {
                let lr = long_size / total;
                let sr = short_size / total;
                let r = if sr > 0.0 { long_size / short_size } else { f64::INFINITY };
                (lr, sr, Some(r))
            } else {
                (0.0, 0.0, None)
            };
            out.push(LongShortRatio {
                symbol: sym.to_string(),
                ratio_type: "pos_size_1m".to_string(),
                long_ratio,
                short_ratio,
                ratio,
                timestamp: ts, ..Default::default() 
            });
        }
        Ok(out)
    }

    // ── Recent public trades ──────────────────────────────────────────────────
    //
    // Source: GET /v2/trades/{symbol}/hist?limit=N
    // Response: [[ID, MTS, AMOUNT, PRICE], ...]
    // AMOUNT > 0 → Buy, AMOUNT < 0 → Sell.
    // Max depth: 10 000 records per call. MTS already in milliseconds.

    async fn get_recent_trades(
        &self,
        symbol: SymbolInput<'_>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> crate::core::ExchangeResult<Vec<crate::core::types::PublicTrade>> {
        let sym = symbol.resolve(crate::core::ExchangeId::Bitfinex, account_type)?;
        let mut query = HashMap::new();
        if let Some(l) = limit {
            query.insert("limit".to_string(), l.min(10000).to_string());
        }
        let raw = self
            .get(BitfinexEndpoint::Trades, &[("symbol", &sym)], query)
            .await?;
        BitfinexParser::parse_recent_trades(&raw)
    }

    // ── Deep trade history (windowed) ─────────────────────────────────────────
    //
    // Source: GET /v2/trades/{symbol}/hist?end=&limit=&sort=-1
    // Live-verified 2026-07-08: `end=<ms>` + `sort=-1` returns records with
    // MTS strictly LESS than `end`, newest-first, up to `limit` (max 10 000).
    // Three consecutive pages (end = min(MTS)_prev_page) showed zero overlap
    // and continuous backward coverage — no discovered ceiling.
    //
    // Bitfinex has no per-trade ID cursor usable for backward paging (trade
    // IDs are venue-internal and not densely walkable) — this connector
    // re-purposes the shared `from_id: Option<u64>` parameter as a
    // millisecond timestamp (the `end` bound) per the `HistoryCursor::
    // TsWindow` contract documented in `backfill::agg_trades_paginated`.
    // Each returned `AggTrade.aggregate_id` is set to that trade's own `MTS`
    // (not the real Bitfinex trade ID) so the shared pagination walk's
    // `min_id` == oldest timestamp in the page, and can compute the next
    // window as `oldest_ts - 1` without dimensional mismatch.
    async fn get_agg_trades(
        &self,
        symbol: SymbolInput<'_>,
        limit: Option<u32>,
        from_id: Option<u64>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<AggTrade>> {
        let sym = symbol.resolve(ExchangeId::Bitfinex, account_type)?;
        let mut query = HashMap::new();
        query.insert("limit".to_string(), limit.unwrap_or(1000).min(10_000).to_string());
        query.insert("sort".to_string(), "-1".to_string());
        if let Some(end_ms) = from_id {
            query.insert("end".to_string(), end_ms.to_string());
        }
        let raw = self
            .get(BitfinexEndpoint::Trades, &[("symbol", &sym)], query)
            .await?;
        BitfinexParser::parse_agg_trades(&raw)
    }
}

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

impl ExchangeIdentity for BitfinexConnector {
    fn exchange_id(&self) -> ExchangeId {
        ExchangeId::Bitfinex
    }

    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 {
        // Bitfinex has no separate testnet URLs; this flag enables paper trading symbol routing
        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 {
        BITFINEX_RATE_CAPS
    }

    fn orderbook_capabilities(&self, _account_type: AccountType) -> OrderbookCapabilities {
        static BITFINEX_CHANNELS: &[WsBookChannel] = &[
            WsBookChannel::delta("book/P0", None, None),
            WsBookChannel::delta("book/P1", None, None),
            WsBookChannel::delta("book/P2", None, None),
            WsBookChannel::delta("book/P3", None, None),
            WsBookChannel::delta("book/P4", None, None),
            WsBookChannel::delta("book/R0", None, None),
        ];
        OrderbookCapabilities {
            ws_depths: &[1, 25, 100, 250],
            ws_default_depth: Some(25),
            rest_max_depth: Some(250),
            rest_depth_values: &[1, 25, 100, 250],
            supports_snapshot: true,
            supports_delta: true,
            update_speeds_ms: &[],
            default_speed_ms: None,
            ws_channels: BITFINEX_CHANNELS,
            checksum: Some(ChecksumInfo {
                algorithm: ChecksumAlgorithm::Crc32Interleaved,
                levels_per_side: 25,
                opt_in: true,
            }),
            has_sequence: false,
            has_prev_sequence: false,
            supports_aggregation: true,
            aggregation_levels: &["P0", "P1", "P2", "P3", "P4", "R0"],
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// 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 BitfinexConnector {
    async fn get_price(
        &self,
        symbol: SymbolInput<'_>,
        account_type: AccountType,
    ) -> ExchangeResult<Price> {
        let symbol = symbol.resolve(ExchangeId::Bitfinex, account_type)?;
        let response = self.get(
            BitfinexEndpoint::Ticker,
            &[("symbol", &*symbol)],
            HashMap::new(),
        ).await?;

        let ticker = BitfinexParser::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::Bitfinex, account_type)?;
        // Use P0 precision (highest aggregation) for best performance
        let response = self.get(
            BitfinexEndpoint::Orderbook,
            &[("symbol", &*symbol), ("precision", "P0")],
            HashMap::new(),
        ).await?;

        BitfinexParser::parse_orderbook(&response)
    }

    async fn get_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        limit: Option<u16>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Bitfinex, account_type)?;
        let candle_key = build_candle_key(&symbol, interval);

        let mut params = HashMap::new();
        if let Some(lim) = limit {
            params.insert("limit".to_string(), lim.min(10000).to_string());
        }
        // Don't use sort=1 — it returns data from 2013. Default (newest-first) + parser.reverse() is correct.

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

        let response = self.get(
            BitfinexEndpoint::Candles,
            &[("candle", &candle_key)],
            params,
        ).await?;

        BitfinexParser::parse_klines(&response)
    }

    async fn get_ticker(
        &self,
        symbol: SymbolInput<'_>,
        account_type: AccountType,
    ) -> ExchangeResult<Ticker> {
        let symbol = symbol.resolve(ExchangeId::Bitfinex, account_type)?;
        let response = self.get(
            BitfinexEndpoint::Ticker,
            &[("symbol", &*symbol)],
            HashMap::new(),
        ).await?;

        BitfinexParser::parse_ticker(&response, &symbol)
    }

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

        // Platform status returns [1] for operative, [0] for maintenance
        if let Some(arr) = response.as_array() {
            if !arr.is_empty() {
                if let Some(status) = arr[0].as_i64() {
                    if status == 1 {
                        return Ok(());
                    }
                }
            }
        }

        Err(ExchangeError::Network("Platform in maintenance".to_string()))
    }

    async fn get_exchange_info(&self, account_type: AccountType) -> ExchangeResult<Vec<SymbolInfo>> {
        // Use Bitfinex v1 symbols_details endpoint (returns array with pair info)
        // Note: v1 is still supported and returns more detail than v2 conf endpoints
        if !self.rate_limit_wait(1, false).await {
            return Err(ExchangeError::RateLimitExceeded {
                retry_after: None,
                message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
            });
        }
        let url = "https://api.bitfinex.com/v1/symbols_details";
        let response = self.http.get(url, &HashMap::new()).await?;
        let info = BitfinexParser::parse_exchange_info(&response, account_type)?;
        self.precision.load_from_symbols(&info);
        Ok(info)
    }

    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 /v2/trades/{symbol}/hist — up to 10 000 records per call.
            has_recent_trades: true,
            // Bitfinex candle timeframes: 1m 3m 5m 15m 30m 1h 2h 3h 4h 6h 8h 12h 1D 1W 14D 1M
            supported_intervals: &[
                "1m", "3m", "5m", "15m", "30m",
                "1h", "2h", "3h", "4h", "6h", "8h", "12h",
                "1d", "1w", "2w", "1M",
            ],
            // Bitfinex accepts up to 10 000 candles per request (capped in get_klines with .min(10000)).
            max_kline_limit: Some(10000),
            // ticker, trades, book, candles channels all available in Bitfinex WS v2.
            has_ws_ticker: true,
            has_ws_trades: true,
            has_ws_orderbook: true,
            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 BitfinexConnector {
    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 formatted_symbol = Self::fmt_symbol(&symbol, account_type);
        let prefix = Self::order_type_prefix(account_type);

        // Amount: positive=buy, negative=sell (apply qty precision to absolute value then re-sign)
        let qty_str = self.precision.qty(&formatted_symbol, quantity);
        let amount_str = match side {
            OrderSide::Buy => qty_str,
            OrderSide::Sell => format!("-{}", qty_str),
        };

        match req.order_type {
            OrderType::Market => {
                let body = json!({
                    "type": format!("{}MARKET", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::Limit { price } => {
                let body = json!({
                    "type": format!("{}LIMIT", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "price": self.precision.price(&formatted_symbol, price),
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::StopMarket { stop_price } => {
                // Bitfinex: EXCHANGE STOP (triggers market at stop_price)
                let body = json!({
                    "type": format!("{}STOP", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "price": self.precision.price(&formatted_symbol, stop_price),
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::StopLimit { stop_price, limit_price } => {
                // Bitfinex: EXCHANGE STOP LIMIT
                let body = json!({
                    "type": format!("{}STOP LIMIT", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "price": self.precision.price(&formatted_symbol, limit_price),
                    "price_aux_limit": self.precision.price(&formatted_symbol, stop_price),
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::TrailingStop { callback_rate, activation_price: _ } => {
                // Bitfinex: EXCHANGE TRAILING STOP
                // trail_pct is callback_rate in percent
                let body = json!({
                    "type": format!("{}TRAILING STOP", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "price": callback_rate.to_string(), // trail distance as % string
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::PostOnly { price } => {
                // Bitfinex: EXCHANGE LIMIT with flags = 4096 (POST_ONLY)
                let body = json!({
                    "type": format!("{}LIMIT", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "price": self.precision.price(&formatted_symbol, price),
                    "flags": 4096,
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::Ioc { price } => {
                // Bitfinex: EXCHANGE IOC
                let price_val = price.unwrap_or(0.0);
                let body = json!({
                    "type": format!("{}IOC", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "price": self.precision.price(&formatted_symbol, price_val),
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::Fok { price } => {
                // Bitfinex: EXCHANGE FOK
                let body = json!({
                    "type": format!("{}FOK", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "price": self.precision.price(&formatted_symbol, price),
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::Iceberg { price, display_quantity } => {
                // Bitfinex: EXCHANGE LIMIT with max_show parameter
                let body = json!({
                    "type": format!("{}LIMIT", prefix),
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "price": self.precision.price(&formatted_symbol, price),
                    "meta": {
                        "max_show": self.precision.qty(&formatted_symbol, display_quantity),
                    },
                    "flags": 64, // Hidden flag
                });
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::ReduceOnly { price } => {
                // Bitfinex: LIMIT with reduce-only flag (only valid for margin/futures)
                if !Self::is_derivatives(account_type) {
                    return Err(ExchangeError::NotImplemented(
                        "ReduceOnly not supported for Spot".to_string()
                    ));
                }
                let order_type_str = if price.is_some() { "LIMIT" } else { "MARKET" };
                let mut body = json!({
                    "type": order_type_str,
                    "symbol": formatted_symbol,
                    "amount": amount_str,
                    "flags": 1024, // REDUCE_ONLY flag
                });
                if let Some(p) = price {
                    body["price"] = json!(self.precision.price(&formatted_symbol, p));
                }
                let response = self.post(BitfinexEndpoint::SubmitOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response).map(PlaceOrderResponse::Simple)
            }

            OrderType::Oto { .. } => Err(ExchangeError::NotImplemented(
                "Oto orders not supported on Bitfinex".into()
            )),
            OrderType::ConditionalPlan { .. } => Err(ExchangeError::NotImplemented(
                "ConditionalPlan orders not supported on Bitfinex".into()
            )),
            OrderType::DcaRecurring { .. } => Err(ExchangeError::NotImplemented(
                "DcaRecurring orders not supported on Bitfinex".into()
            )),
            _ => 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 } => {
                let id = order_id.parse::<i64>()
                    .map_err(|_| ExchangeError::InvalidRequest("Invalid order ID".to_string()))?;

                let body = json!({ "id": id });
                let response = self.post(BitfinexEndpoint::CancelOrder, &[], body).await?;
                BitfinexParser::parse_submit_order(&response)
            }

            CancelScope::Batch { ref order_ids } => {
                // Bitfinex: POST /auth/w/order/cancel/multi with {"id": [...]}
                let ids: Vec<i64> = order_ids.iter()
                    .filter_map(|id| id.parse::<i64>().ok())
                    .collect();

                if ids.is_empty() {
                    return Err(ExchangeError::InvalidRequest("No valid order IDs".to_string()));
                }

                let body = json!({ "id": ids });
                let _response = self.post(BitfinexEndpoint::CancelMultipleOrders, &[], body).await?;

                // Return a placeholder — Bitfinex multi-cancel returns notifications, not a single order
                Ok(Order {
                    id: ids[0].to_string(),
                    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 for All/BySymbol", req.scope)
            )),
        }
    }

    async fn get_order_history(
        &self,
        filter: OrderHistoryFilter,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<Order>> {
        let mut body = json!({});

        if let Some(sym) = &filter.symbol {
            // sym is already a Symbol struct
            let formatted_symbol = Self::fmt_symbol(sym, _account_type);
            body["symbol"] = json!(formatted_symbol);
        }

        if let Some(start) = filter.start_time {
            body["start"] = json!(start);
        }
        if let Some(end) = filter.end_time {
            body["end"] = json!(end);
        }
        if let Some(limit) = filter.limit {
            body["limit"] = json!(limit.min(2500));
        }

        let response = self.post(BitfinexEndpoint::OrderHistory, &[], body).await?;
        BitfinexParser::parse_orders(&response)
    }

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

        let formatted_symbol = Self::fmt_symbol(&symbol, account_type);

        let body = json!({ "symbol": formatted_symbol });

        let response = self.post(
            BitfinexEndpoint::ActiveOrdersBySymbol,
            &[("symbol", &formatted_symbol)],
            body,
        ).await?;

        let orders = BitfinexParser::parse_orders(&response)?;

        orders.into_iter()
            .find(|o| o.id == order_id)
            .ok_or_else(|| ExchangeError::Parse(format!("Order {} not found", order_id)))
    }

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

        let response = if let Some(s) = symbol {
            let formatted_symbol = Self::fmt_symbol(&s, account_type);
            self.post(
                BitfinexEndpoint::ActiveOrdersBySymbol,
                &[("symbol", &formatted_symbol)],
                json!({}),
            ).await?
        } else {
            self.post(
                BitfinexEndpoint::ActiveOrders,
                &[],
                json!({}),
            ).await?
        };

        BitfinexParser::parse_orders(&response)
    }

    async fn get_user_trades(
        &self,
        filter: UserTradeFilter,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<UserTrade>> {
        // Build request body — Bitfinex uses POST with JSON body for authenticated endpoints
        let mut body = json!({ "sort": -1 });

        if let Some(start) = filter.start_time {
            body["start"] = json!(start);
        }
        if let Some(end) = filter.end_time {
            body["end"] = json!(end);
        }
        if let Some(lim) = filter.limit {
            body["limit"] = json!(lim.min(250));
        }

        // Use symbol-scoped endpoint when symbol is provided
        let response = if let Some(sym_raw) = &filter.symbol {
            // Parse symbol string "BTC/USDT" or raw "tBTCUSD"
            let formatted = if sym_raw.starts_with('t') || sym_raw.starts_with('f') {
                // Already in Bitfinex format
                sym_raw.clone()
            } else {
                let parts: Vec<&str> = sym_raw.split('/').collect();
                if parts.len() == 2 {
                    let s = Symbol::new(parts[0], parts[1]);
                    Self::fmt_symbol(&s, account_type)
                } else {
                    format!("t{}", sym_raw.to_uppercase())
                }
            };
            self.post(
                BitfinexEndpoint::TradeHistoryBySymbol,
                &[("symbol", &formatted)],
                body,
            ).await?
        } else {
            self.post(BitfinexEndpoint::TradeHistory, &[], body).await?
        };

        BitfinexParser::parse_user_trades(&response)
    }

    fn trading_capabilities(&self, _account_type: AccountType) -> TradingCapabilities {
        // All order types (Market, Limit, Stop, StopLimit, TrailingStop, PostOnly, IOC, FOK,
        // Iceberg) work for both Spot and Derivatives. The only Derivatives-only order type is
        // ReduceOnly, but TradingCapabilities has no has_reduce_only field — the gate is
        // enforced at runtime in place_order(). Wire format differences (the "EXCHANGE " prefix
        // for Spot) are handled transparently by order_type_prefix(). No per-account branching.
        TradingCapabilities {
            has_market_order: true,
            has_limit_order: true,
            has_stop_market: true,  // "EXCHANGE STOP" (Spot) / "STOP" (Derivatives)
            has_stop_limit: true,   // "EXCHANGE STOP LIMIT" / "STOP LIMIT"
            has_trailing_stop: true, // "EXCHANGE TRAILING STOP" / "TRAILING STOP"
            // Bitfinex has no bracket (TP+SL combo) order type.
            has_bracket: false,
            // Bitfinex has no OCO order type.
            has_oco: false,
            has_amend: true,        // AmendOrder trait implemented via UpdateOrder endpoint
            has_batch: true,        // BatchOrders trait implemented via OrderMulti endpoint
            // Bitfinex OrderMulti accepts up to 75 operations per call.
            max_batch_size: Some(75),
            has_cancel_all: true,   // CancelAll trait implemented via CancelMultipleOrders
            has_user_trades: true,
            has_order_history: true,
        }
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl Account for BitfinexConnector {
    async fn get_balance(&self, _query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
        let response = self.post(
            BitfinexEndpoint::Wallets,
            &[],
            json!({}),
        ).await?;

        BitfinexParser::parse_balances(&response)
    }

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

        Ok(AccountInfo {
            account_type,
            can_trade: true,
            can_withdraw: true,
            can_deposit: true,
            maker_commission: 0.1,  // Default Bitfinex fees
            taker_commission: 0.2,
            balances,
        })
    }

    async fn get_fees(&self, symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
        // Bitfinex: POST /auth/r/trades/hist returns trades with fee info
        // Use a recent trade to get actual fee rates, or return defaults from account summary
        let body = json!({ "limit": 1 });
        let response = self.post(BitfinexEndpoint::TradeHistory, &[], body).await?;

        // Try to extract fee from most recent trade
        if let Some(arr) = response.as_array() {
            if let Some(trade) = arr.first() {
                if let Some(trade_arr) = trade.as_array() {
                    // Trade array format: [ID, PAIR, MTS_CREATE, ORDER_ID, EXEC_AMOUNT, EXEC_PRICE, ORDER_TYPE, ORDER_PRICE, MAKER, FEE, FEE_CURRENCY]
                    if trade_arr.len() > 9 {
                        let fee = trade_arr[9].as_f64().unwrap_or(0.0).abs();
                        let amount = trade_arr[4].as_f64().unwrap_or(1.0).abs();
                        let price = trade_arr[5].as_f64().unwrap_or(1.0);
                        let rate = if amount * price > 0.0 { fee / (amount * price) } else { 0.002 };

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

        // Fallback: default Bitfinex fees
        Ok(FeeInfo {
            maker_rate: 0.001,  // 0.1%
            taker_rate: 0.002,  // 0.2%
            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);

        AccountCapabilities {
            has_balances: true,
            has_account_info: true,
            has_fees: true,
            has_transfers: true,        // AccountTransfers trait implemented (wallet-to-wallet)
            has_sub_accounts: true,     // SubAccounts trait implemented (list + transfer)
            has_deposit_withdraw: true, // CustodialFunds trait implemented (deposit address + withdraw + movements)
            // Bitfinex margin borrowing is order-flag-based, no dedicated borrow/repay endpoint.
            has_margin: false,
            // No earn or staking product endpoints in this connector.
            has_earn_staking: false,
            // Funding payments (ledger category 28) are perpetual interest charges on open
            // derivative positions — not applicable to Spot accounts.
            has_funding_history: is_futures,
            has_ledger: true,           // AccountLedger trait works for all account types
            // No coin-to-coin conversion endpoint implemented.
            has_convert: false,
            // Positions trait implemented; applies to margin/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 BitfinexConnector {
    async fn get_positions(&self, query: PositionQuery) -> ExchangeResult<Vec<Position>> {
        let account_type = query.account_type;

        if account_type == AccountType::Spot {
            return Err(ExchangeError::NotImplemented(
                "Positions not supported for Spot".to_string()
            ));
        }

        let response = self.post(
            BitfinexEndpoint::Positions,
            &[],
            json!({}),
        ).await?;

        BitfinexParser::parse_positions(&response)
    }

    async fn get_funding_rate(
        &self,
        _symbol: &str,
        account_type: AccountType,
    ) -> ExchangeResult<crate::core::FundingRate> {
        match account_type {
            AccountType::Spot | AccountType::Margin
            | AccountType::Earn | AccountType::Lending
            | AccountType::Options | AccountType::Convert => {
                return Err(ExchangeError::NotImplemented(
                    "Funding rate not supported for Spot/Margin".to_string()
                ));
            }
            _ => {}
        }

        // Bitfinex doesn't have a direct funding rate endpoint for perpetuals
        // Would need to implement via derivatives API or funding book
        Err(ExchangeError::NotImplemented(
            "Funding rate endpoint not implemented for Bitfinex".to_string()
        ))
    }

    async fn modify_position(&self, req: PositionModification) -> ExchangeResult<()> {
        match req {
            PositionModification::SetLeverage { account_type, .. } => {
                if account_type == AccountType::Spot {
                    return Err(ExchangeError::NotImplemented(
                        "Leverage not supported for Spot".to_string()
                    ));
                }
                // Bitfinex handles leverage via order flags, not a separate endpoint
                Err(ExchangeError::NotImplemented(
                    "Set leverage not available - use order flags instead".to_string()
                ))
            }
            _ => Err(ExchangeError::NotImplemented(
                format!("{:?} not supported on {:?}", req, self.exchange_id())
            )),
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// 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 BitfinexConnector {
    async fn cancel_all_orders(
        &self,
        scope: CancelScope,
        _account_type: AccountType,
    ) -> ExchangeResult<CancelAllResponse> {
        match scope {
            CancelScope::All { symbol: None } => {
                // Cancel all orders across all symbols
                let body = json!({ "all": 1 });
                let _response = self.post(BitfinexEndpoint::CancelMultipleOrders, &[], body).await?;

                Ok(CancelAllResponse {
                    cancelled_count: 0, // Bitfinex doesn't return count
                    failed_count: 0,
                    details: vec![],
                })
            }

            CancelScope::All { symbol: Some(sym) } | CancelScope::BySymbol { symbol: sym } => {
                // Cancel all orders for a specific symbol
                let formatted_symbol = Self::fmt_symbol(&sym, _account_type);
                let body = json!({ "symbol": formatted_symbol });
                let _response = self.post(BitfinexEndpoint::CancelMultipleOrders, &[], body).await?;

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

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

// ═══════════════════════════════════════════════════════════════════════════════
// AMEND ORDER (optional trait)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AmendOrder for BitfinexConnector {
    async fn amend_order(&self, req: AmendRequest) -> ExchangeResult<Order> {
        let id = req.order_id.parse::<i64>()
            .map_err(|_| ExchangeError::InvalidRequest("Invalid order ID".to_string()))?;

        let symbol = &req.symbol;
        let formatted_symbol = Self::fmt_symbol(symbol, req.account_type);

        let mut body = json!({ "id": id });

        if let Some(price) = req.fields.price {
            body["price"] = json!(self.precision.price(&formatted_symbol, price));
        }
        if let Some(qty) = req.fields.quantity {
            // For Bitfinex, amount sign determines buy/sell — preserve original sign
            body["amount"] = json!(self.precision.qty(&formatted_symbol, qty));
        }
        if let Some(stop_price) = req.fields.trigger_price {
            body["price_aux_limit"] = json!(self.precision.price(&formatted_symbol, stop_price));
        }

        let response = self.post(BitfinexEndpoint::UpdateOrder, &[], body).await?;
        BitfinexParser::parse_submit_order(&response)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// 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 BitfinexConnector {
    /// Place multiple orders in a single batch request.
    ///
    /// Bitfinex endpoint: POST /v2/auth/w/order/multi
    /// Body: `{"ops": [["on", {...order_params}], ...]}`
    /// Max 75 operations per request.
    async fn place_orders_batch(
        &self,
        orders: Vec<OrderRequest>,
    ) -> ExchangeResult<Vec<OrderResult>> {
        if orders.is_empty() {
            return Ok(vec![]);
        }

        let ops: Vec<Value> = orders.iter().map(|req| {
            let account_type = req.account_type;
            let formatted_symbol = Self::fmt_symbol(&req.symbol, account_type);
            let prefix = Self::order_type_prefix(account_type);

            let qty_str = self.precision.qty(&formatted_symbol, req.quantity);
            let amount_str = match req.side {
                OrderSide::Buy => qty_str,
                OrderSide::Sell => format!("-{}", qty_str),
            };

            let (order_type_str, price, price_aux) = match &req.order_type {
                OrderType::Market => (format!("{}MARKET", prefix), None, None),
                OrderType::Limit { price } => (format!("{}LIMIT", prefix), Some(*price), None),
                OrderType::StopMarket { stop_price } => (format!("{}STOP", prefix), Some(*stop_price), None),
                OrderType::StopLimit { stop_price, limit_price } => (
                    format!("{}STOP LIMIT", prefix),
                    Some(*limit_price),
                    Some(*stop_price),
                ),
                OrderType::PostOnly { price } => (format!("{}LIMIT", prefix), Some(*price), None),
                OrderType::Ioc { price } => (format!("{}IOC", prefix), price.map(|p| p), None),
                OrderType::Fok { price } => (format!("{}FOK", prefix), Some(*price), None),
                _ => (format!("{}MARKET", prefix), None, None),
            };

            let mut order_obj = json!({
                "type": order_type_str,
                "symbol": formatted_symbol,
                "amount": amount_str,
            });

            if let Some(p) = price {
                order_obj["price"] = json!(self.precision.price(&formatted_symbol, p));
            }
            if let Some(aux) = price_aux {
                order_obj["price_aux_limit"] = json!(self.precision.price(&formatted_symbol, aux));
            }

            // PostOnly flag
            if matches!(req.order_type, OrderType::PostOnly { .. }) {
                order_obj["flags"] = json!(4096);
            }

            json!(["on", order_obj])
        }).collect();

        let body = json!({ "ops": ops });
        let response = self.post(BitfinexEndpoint::OrderMulti, &[], body).await?;

        // Bitfinex returns array of notification arrays
        // Each notification: [0, "on-req", null, null, [order_array], ...]
        // We parse what we can from the response
        let results = if let Some(arr) = response.as_array() {
            arr.iter().enumerate().map(|(i, item)| {
                // Try to extract order from notification
                let order_arr = item.as_array()
                    .and_then(|a| a.get(4))
                    .and_then(|v| v.as_array());

                if let Some(order_data) = order_arr {
                    if let Some(id_val) = order_data.first() {
                        if let Some(id) = id_val.as_i64() {
                            let order = orders.get(i);
                            return OrderResult {
                                order: Some(Order {
                                    id: id.to_string(),
                                    client_order_id: None,
                                    symbol: order.map(|o| o.symbol.to_string()),
                                    side: order.map(|o| o.side).unwrap_or(OrderSide::Buy),
                                    order_type: order.map(|o| o.order_type.clone()).unwrap_or(OrderType::Market),
                                    status: crate::core::OrderStatus::New,
                                    price: None,
                                    stop_price: None,
                                    quantity: order.map(|o| o.quantity).unwrap_or(0.0),
                                    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,
                                }),
                                client_order_id: None,
                                success: true,
                                error: None,
                                error_code: None,
                            };
                        }
                    }
                }

                OrderResult {
                    order: None,
                    client_order_id: None,
                    success: false,
                    error: Some("Failed to parse batch order response".to_string()),
                    error_code: None,
                }
            }).collect()
        } else {
            orders.iter().map(|_| OrderResult {
                order: None,
                client_order_id: None,
                success: false,
                error: Some("Unexpected response format".to_string()),
                error_code: None,
            }).collect()
        };

        Ok(results)
    }

    /// Cancel multiple orders in a single batch request.
    ///
    /// Bitfinex endpoint: POST /v2/auth/w/order/multi
    /// Body: `{"ops": [["oc", {"id": N}], ...]}`
    async fn cancel_orders_batch(
        &self,
        order_ids: Vec<String>,
        _symbol: Option<&str>,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<OrderResult>> {
        if order_ids.is_empty() {
            return Ok(vec![]);
        }

        let ops: Vec<Value> = order_ids.iter()
            .filter_map(|id| id.parse::<i64>().ok())
            .map(|id| json!(["oc", { "id": id }]))
            .collect();

        if ops.is_empty() {
            return Err(ExchangeError::InvalidRequest("No valid order IDs".to_string()));
        }

        let body = json!({ "ops": ops });
        let _response = self.post(BitfinexEndpoint::OrderMulti, &[], body).await?;

        // Return success results — Bitfinex returns notifications, not per-order status
        let results = order_ids.iter().map(|_id| OrderResult {
            order: None,
            client_order_id: None,
            success: true,
            error: None,
            error_code: None,
        }).collect();

        let _ = order_ids; // silence unused after move
        Ok(results)
    }

    /// Maximum batch place size (Bitfinex limit: 75 operations per request).
    fn max_batch_place_size(&self) -> usize {
        75
    }

    /// Maximum batch cancel size (Bitfinex limit: 75 operations per request).
    fn max_batch_cancel_size(&self) -> usize {
        75
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// 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 BitfinexConnector {
    /// Transfer between Bitfinex wallets (exchange/margin/funding).
    ///
    /// Endpoint: POST /v2/auth/w/transfer
    /// Body: from, to, currency, currency_to, amount
    async fn transfer(&self, req: TransferRequest) -> ExchangeResult<TransferResponse> {
        fn account_type_to_wallet(account_type: AccountType) -> &'static str {
            match account_type {
                AccountType::Spot => "exchange",
                AccountType::Margin => "margin",
                AccountType::FuturesCross | AccountType::FuturesIsolated => "funding",
                AccountType::Lending => "funding",
                AccountType::Earn | AccountType::Options | AccountType::Convert => "exchange",
            }
        }

        let from_wallet = account_type_to_wallet(req.from_account);
        let to_wallet = account_type_to_wallet(req.to_account);

        let body = json!({
            "from": from_wallet,
            "to": to_wallet,
            "currency": req.asset.to_uppercase(),
            "currency_to": req.asset.to_uppercase(),
            "amount": req.amount.to_string(),
        });

        let response = self.post(BitfinexEndpoint::Transfer, &[], body).await?;

        // Response: [1, "SUCCESS", null, "transfer", [...transfer_data]]
        // Or: [0, "ERROR", null, "transfer", "error message"]
        if let Some(arr) = response.as_array() {
            let mts_created = arr.get(4)
                .and_then(|v| v.as_array())
                .and_then(|inner| inner.first())
                .and_then(|v| v.as_i64());

            return Ok(TransferResponse {
                transfer_id: format!("{}", mts_created.unwrap_or(0)),
                status: "Successful".to_string(),
                asset: req.asset,
                amount: req.amount,
                timestamp: mts_created,
            });
        }

        Err(ExchangeError::Parse("Unexpected transfer response format".to_string()))
    }

    /// Transfer history is not available via a standard endpoint on Bitfinex.
    ///
    /// Returns an empty vec — use movements endpoint for deposit/withdrawal history instead.
    async fn get_transfer_history(
        &self,
        _filter: TransferHistoryFilter,
    ) -> ExchangeResult<Vec<TransferResponse>> {
        Ok(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 BitfinexConnector {
    /// Get deposit address for an asset on a given network/method.
    ///
    /// Endpoint: POST /v2/auth/w/deposit/address
    /// Body: wallet (exchange), method (network/coin), op_renew (0/1)
    async fn get_deposit_address(
        &self,
        asset: &str,
        network: Option<&str>,
    ) -> ExchangeResult<DepositAddress> {
        let method = network.unwrap_or(asset).to_lowercase();

        let body = json!({
            "wallet": "exchange",
            "method": method,
            "op_renew": 0,
        });

        let response = self.post(BitfinexEndpoint::DepositAddress, &[], body).await?;

        // Response: [MTS, TYPE, MESSAGE_ID, null, [nil, METHOD, CURRENCY_CODE, nil, nil, ADDRESS, ...]]
        if let Some(arr) = response.as_array() {
            if let Some(inner) = arr.get(4).and_then(|v| v.as_array()) {
                let address = inner.get(5)
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let tag = inner.get(6)
                    .and_then(|v| v.as_str())
                    .filter(|s| !s.is_empty())
                    .map(|s| s.to_string());

                return Ok(DepositAddress {
                    address,
                    tag,
                    network: Some(method),
                    asset: asset.to_string(),
                    created_at: None,
                });
            }
        }

        Err(ExchangeError::Parse("Unexpected deposit address response format".to_string()))
    }

    /// Submit a withdrawal request.
    ///
    /// Endpoint: POST /v2/auth/w/withdraw
    /// Body: wallet, method, amount, address, payment_id (tag)
    async fn withdraw(&self, req: WithdrawRequest) -> ExchangeResult<WithdrawResponse> {
        let method = req.network
            .as_deref()
            .unwrap_or(&req.asset)
            .to_lowercase();

        let mut body = json!({
            "wallet": "exchange",
            "method": method,
            "amount": req.amount.to_string(),
            "address": req.address,
        });

        if let Some(tag) = &req.tag {
            body["payment_id"] = json!(tag);
        }

        let response = self.post(BitfinexEndpoint::Withdraw, &[], body).await?;

        // Response: [MTS, TYPE, MESSAGE_ID, null, [WITHDRAWAL_ID, ...]]
        if let Some(arr) = response.as_array() {
            let withdraw_id = arr.get(4)
                .and_then(|v| v.as_array())
                .and_then(|inner| inner.first())
                .and_then(|v| v.as_i64())
                .map(|id| id.to_string())
                .unwrap_or_else(|| "0".to_string());

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

        Err(ExchangeError::Parse("Unexpected withdraw response format".to_string()))
    }

    /// Get deposit and/or withdrawal history via movements endpoint.
    ///
    /// Endpoint: POST /v2/auth/r/movements/{Symbol}/hist
    /// Movements cover both deposits (positive amount) and withdrawals (negative amount).
    async fn get_funds_history(
        &self,
        filter: FundsHistoryFilter,
    ) -> ExchangeResult<Vec<FundsRecord>> {
        let symbol = filter.asset.as_deref().unwrap_or("").to_uppercase();
        let symbol_path = if symbol.is_empty() { "".to_string() } else { symbol.clone() };

        let mut body = json!({});
        if let Some(start) = filter.start_time {
            body["start"] = json!(start);
        }
        if let Some(end) = filter.end_time {
            body["end"] = json!(end);
        }
        if let Some(limit) = filter.limit {
            body["limit"] = json!(limit.min(1000));
        }

        let response = self.post(
            BitfinexEndpoint::Movements,
            &[("symbol", &symbol_path)],
            body,
        ).await?;

        // Movements array: each element is:
        // [ID, CURRENCY, CURRENCY_NAME, nil, nil, MTS_STARTED, MTS_UPDATED, nil, nil,
        //  STATUS, nil, nil, AMOUNT, FEES, nil, DESTINATION_ADDRESS, nil, nil, nil, TRANSACTION_ID, ...]
        let records = if let Some(arr) = response.as_array() {
            arr.iter().filter_map(|item| {
                let m = item.as_array()?;
                let id = m.first()?.as_i64()?.to_string();
                let currency = m.get(1)?.as_str().unwrap_or("").to_string();
                let timestamp = m.get(5)?.as_i64().unwrap_or(0);
                let status = m.get(9)?.as_str().unwrap_or("Unknown").to_string();
                let amount = m.get(12)?.as_f64().unwrap_or(0.0);
                let _fee = m.get(13).and_then(|v| v.as_f64());
                let address = m.get(15).and_then(|v| v.as_str()).unwrap_or("").to_string();
                let tx_hash = m.get(20).and_then(|v| v.as_str())
                    .filter(|s| !s.is_empty())
                    .map(|s| s.to_string());

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

                if amount >= 0.0 {
                    // 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
                    }
                } else {
                    // Withdrawal (negative amount)
                    if matches!(filter.record_type, FundsRecordType::Withdrawal | FundsRecordType::Both) {
                        Some(FundsRecord::Withdrawal {
                            id,
                            asset: currency,
                            amount: amount.abs(),
                            fee: _fee.map(|f| f.abs()),
                            address,
                            tag: None,
                            tx_hash,
                            network: None,
                            status,
                            timestamp,
                        })
                    } else {
                        None
                    }
                }
            }).collect()
        } else {
            vec![]
        };

        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 BitfinexConnector {
    /// Perform sub-account operations (list, transfer; create/get_balance not supported).
    async fn sub_account_operation(
        &self,
        op: SubAccountOperation,
    ) -> ExchangeResult<SubAccountResult> {
        match op {
            SubAccountOperation::List => {
                let response = self.post(BitfinexEndpoint::SubAccountList, &[], json!({})).await?;

                // Response is an array of sub-account objects
                let accounts = if let Some(arr) = response.as_array() {
                    arr.iter().filter_map(|item| {
                        let obj = item.as_object()?;
                        let id = obj.get("id")?.as_i64()?.to_string();
                        let name = obj.get("email")
                            .or_else(|| obj.get("name"))
                            .and_then(|v| v.as_str())
                            .unwrap_or("")
                            .to_string();
                        let status = obj.get("status")
                            .and_then(|v| v.as_str())
                            .unwrap_or("Normal")
                            .to_string();
                        Some(SubAccount { id, name, status })
                    }).collect()
                } else {
                    vec![]
                };

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

            SubAccountOperation::Transfer { sub_account_id, asset, amount, to_sub } => {
                // Bitfinex sub-account transfer: POST /v2/auth/w/sub_account/transfer
                // Body: sub_account_id, wallet_from, wallet_to, amount, currency
                // Bitfinex uses "exchange" wallet for both master→sub and sub→master transfers
                let (wallet_from, wallet_to) = ("exchange", "exchange");

                let body = json!({
                    "sub_account_id": sub_account_id.parse::<i64>().unwrap_or(0),
                    "wallet_from": wallet_from,
                    "wallet_to": wallet_to,
                    "amount": amount.to_string(),
                    "currency": asset.to_uppercase(),
                    "to_sub": to_sub,
                });

                let response = self.post(BitfinexEndpoint::SubAccountTransfer, &[], body).await?;

                let transaction_id = response.as_array()
                    .and_then(|arr| arr.get(4))
                    .and_then(|v| v.as_array())
                    .and_then(|inner| inner.first())
                    .and_then(|v| v.as_i64())
                    .map(|id| id.to_string());

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

            SubAccountOperation::Create { .. } => {
                Err(ExchangeError::NotImplemented(
                    "Create sub-account not supported via Bitfinex REST API".to_string()
                ))
            }

            SubAccountOperation::GetBalance { .. } => {
                Err(ExchangeError::NotImplemented(
                    "Get sub-account balance not supported via standard Bitfinex REST API".to_string()
                ))
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// FUNDING HISTORY (optional trait)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl FundingHistory for BitfinexConnector {
    /// Get historical funding payments for the account.
    ///
    /// Uses `POST /v2/auth/r/ledgers/{currency}/hist` with `{"category": 28}`.
    /// Category 28 = funding charges/payments on perpetual positions.
    ///
    /// `filter.symbol` is treated as the settlement currency (e.g. "UST", "BTC").
    /// When `None`, defaults to "UST" (Bitfinex perpetual settlement asset).
    async fn get_funding_payments(
        &self,
        filter: FundingFilter,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<FundingPayment>> {
        // Bitfinex uses currency-level ledger queries; derive currency from symbol or use UST
        let currency = filter.symbol
            .as_deref()
            .map(|s| {
                // Symbol may be like "tBTCF0:USTF0" — extract the settlement currency
                if let Some(idx) = s.rfind(':') {
                    // e.g. "USTF0" → strip "F0"
                    s[idx + 1..].trim_end_matches("F0").to_uppercase()
                } else {
                    s.to_uppercase()
                }
            })
            .unwrap_or_else(|| "UST".to_string());

        // Build POST body: category 28 = funding charges
        let mut body = json!({"category": 28});

        if let Some(start) = filter.start_time {
            body["start"] = json!(start);
        }
        if let Some(end) = filter.end_time {
            body["end"] = json!(end);
        }
        if let Some(limit) = filter.limit {
            body["limit"] = json!(limit.min(500));
        }

        let response = self.post(
            BitfinexEndpoint::LedgerHist,
            &[("currency", &currency)],
            body,
        ).await?;

        // Response: array of arrays [[ID, CURRENCY, null, MTS, null, AMOUNT, BALANCE, null, DESCRIPTION], ...]
        let entries = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array for ledger response".to_string()))?;

        let payments = entries.iter().filter_map(|row| {
            let arr = row.as_array()?;

            let id = arr.first()?.as_i64()?.to_string();
            let asset = arr.get(1)?.as_str()?.to_string();
            let timestamp = arr.get(3)?.as_i64()?;
            let amount = arr.get(5)?.as_f64()?;
            let balance = arr.get(6).and_then(|v| v.as_f64());
            let description = arr.get(8)
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();

            // Derive symbol from the description when possible (Bitfinex includes it)
            // Description format: "Margin funding payment on wallet margin for BTC/USD @ 0.001"
            // Use id as ref; payment amount is the actual funding payment
            let _ = (id, balance, description);

            Some(FundingPayment {
                // Bitfinex ledger doesn't carry per-entry symbol/instrument info directly
                symbol: currency.clone(),
                // Funding rate is not returned in ledger entries (only in separate funding data)
                funding_rate: 0.0,
                // Position size not available in ledger entries
                position_size: 0.0,
                payment: amount,
                asset,
                timestamp,
            })
        }).collect();

        Ok(payments)
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// ACCOUNT LEDGER (optional trait)
// ═══════════════════════════════════════════════════════════════════════════════

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AccountLedger for BitfinexConnector {
    /// Get account ledger entries.
    ///
    /// Uses `POST /v2/auth/r/ledgers/{currency}/hist`.
    /// When `filter.asset` is `None`, queries with currency "USD" as default.
    /// To fetch all currencies, callers should query per currency.
    ///
    /// Bitfinex ledger categories:
    /// - 1  = deposit
    /// - 2  = withdrawal
    /// - 4  = exchange
    /// - 5  = margin (trade)
    /// - 28 = funding (perpetual interest)
    /// - 68 = affiliates earning
    async fn get_ledger(
        &self,
        filter: LedgerFilter,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<LedgerEntry>> {
        let currency = filter.asset
            .as_deref()
            .unwrap_or("USD")
            .to_uppercase();

        // Map entry_type filter to Bitfinex category
        let category: Option<i32> = filter.entry_type.as_ref().map(|t| match t {
            LedgerEntryType::Deposit   => 1,
            LedgerEntryType::Withdrawal => 2,
            LedgerEntryType::Trade     => 5,
            LedgerEntryType::Funding   => 28,
            LedgerEntryType::Fee       => 4,
            LedgerEntryType::Rebate    => 4,
            LedgerEntryType::Transfer  => 2,
            LedgerEntryType::Liquidation => 5,
            LedgerEntryType::Settlement  => 5,
            LedgerEntryType::Other(_)    => -1, // -1 = no category filter
        });

        let mut body = json!({});
        if let Some(cat) = category {
            if cat >= 0 {
                body["category"] = json!(cat);
            }
        }
        if let Some(start) = filter.start_time {
            body["start"] = json!(start);
        }
        if let Some(end) = filter.end_time {
            body["end"] = json!(end);
        }
        if let Some(limit) = filter.limit {
            body["limit"] = json!(limit.min(500));
        }

        let response = self.post(
            BitfinexEndpoint::LedgerHist,
            &[("currency", &currency)],
            body,
        ).await?;

        // Response: [[ID, CURRENCY, null, MTS, null, AMOUNT, BALANCE, null, DESCRIPTION], ...]
        let entries = response.as_array()
            .ok_or_else(|| ExchangeError::Parse("Expected array for ledger response".to_string()))?;

        let ledger: Vec<LedgerEntry> = entries.iter().filter_map(|row| {
            let arr = row.as_array()?;

            let id = arr.first()?.as_i64()?.to_string();
            let asset = arr.get(1)?.as_str().unwrap_or(&currency).to_string();
            let timestamp = arr.get(3)?.as_i64()?;
            let amount = arr.get(5)?.as_f64()?;
            let balance = arr.get(6).and_then(|v| v.as_f64());
            let description = arr.get(8)
                .and_then(|v| v.as_str())
                .unwrap_or("")
                .to_string();

            // Classify the entry type from description keywords
            let entry_type = classify_bitfinex_ledger_entry(&description);

            Some(LedgerEntry {
                id,
                asset,
                amount,
                balance,
                entry_type,
                description,
                ref_id: None,
                timestamp,
            })
        }).collect();

        Ok(ledger)
    }
}

/// Classify a Bitfinex ledger entry type from its description string.
fn classify_bitfinex_ledger_entry(description: &str) -> LedgerEntryType {
    let lower = description.to_lowercase();
    if lower.contains("deposit") || lower.contains("crypto deposit") {
        LedgerEntryType::Deposit
    } else if lower.contains("withdraw") {
        LedgerEntryType::Withdrawal
    } else if lower.contains("transfer") {
        LedgerEntryType::Transfer
    } else if lower.contains("margin funding") || lower.contains("funding payment") {
        LedgerEntryType::Funding
    } else if lower.contains("trading fee") || lower.contains("taker fee") || lower.contains("maker fee") {
        LedgerEntryType::Fee
    } else if lower.contains("rebate") {
        LedgerEntryType::Rebate
    } else if lower.contains("liquidat") {
        LedgerEntryType::Liquidation
    } else if lower.contains("settlement") || lower.contains("settle") {
        LedgerEntryType::Settlement
    } else if lower.contains("trade") || lower.contains("exchange") || lower.contains("order") {
        LedgerEntryType::Trade
    } else {
        LedgerEntryType::Other(description.to_string())
    }
}

impl crate::core::traits::HasCapabilities for BitfinexConnector {
    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
            has_liquidation_history: false,
            // OI from /v2/status/deriv/{key}/hist idx 18 — event snapshots
            has_open_interest_history: true,
            has_premium_index: false,
            // LSR from /v2/stats1/pos.size:1m:{sym}:{side}/hist (spot confirmed; perp unverified)
            has_long_short_ratio_history: true,
            // Funding from /v2/status/deriv/{key}/hist idx 12 — event snapshots
            has_funding_rate_history: true,
            has_basis_history: false,
            has_taker_volume_history: false,
            has_liquidation_bucket_history: false,
            has_insurance_fund: true,
            // No dedicated kline endpoints for mark/index/premium — wire-absent
            has_mark_price_klines: false,
            has_index_price_klines: false,
            has_premium_index_klines: false,
            // get_agg_trades: /v2/trades/{symbol}/hist windowed pagination —
            // deep beyond get_recent_trades's single shallow page.
            has_agg_trades: true,
            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: true,
            has_batch_place: true, has_batch_cancel: true,
            max_batch_place_size: 75, max_batch_cancel_size: 75,
            has_balance: true, has_account_info: true, has_fees: true,
            has_transfers: true, has_deposit_withdraw: true, has_sub_accounts: true,
            has_funding_payments: true, has_ledger: true,
            has_websocket: true, has_ws_klines: true, has_ws_trades: true,
            has_ws_orderbook: true, has_ws_ticker: true,
            has_ws_mark_price: true, has_ws_funding_rate: true,
            validation: self.validation_status(),
        }
    }

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

    fn trade_history_capabilities(&self) -> crate::core::types::TradeHistoryCapabilities {
        use crate::core::types::{TradeHistoryTier, HistoryCursor};
        // Wave 2 (2026-07-08): `get_agg_trades` now wires `/v2/trades/
        // {symbol}/hist` with `end`/`sort=-1` window pagination. Live-verified:
        // 3 consecutive pages (end = min(MTS)_prev_page) showed zero overlap,
        // continuous backward coverage, no discovered ceiling — effectively
        // unbounded. 10 000/page (existing get_recent_trades limit).
        crate::core::types::TradeHistoryCapabilities {
            spot: TradeHistoryTier::RestWindow { cursor: HistoryCursor::TsWindow, max_back_ms: 0 },
            futures: TradeHistoryTier::RestWindow { cursor: HistoryCursor::TsWindow, max_back_ms: 0 },
            kline_backpage: true,
        }
    }

    fn kline_interval_capabilities(&self) -> crate::core::types::KlineIntervalCapabilities {
        // Probe 2026-07-08: GET /v2/candles/trade:1s:tBTCUSD/hist and the
        // bogus /trade:2s:tBTCUSD/hist both return `[]` — Bitfinex's path
        // segment isn't validated server-side (silent empty for any
        // unknown timeframe), so the live wire is inconclusive either way.
        // Falling back to the official documented timeframe list (matches
        // the connector's own `map_kline_interval` in endpoints.rs
        // exactly): no seconds tier on either spot or margin/derivatives
        // (same candle service backs both — Bitfinex doesn't split
        // spot vs futures kline intervals).
        crate::core::types::KlineIntervalCapabilities {
            spot: &[
                "1m", "3m", "5m", "15m", "30m", "1h", "2h", "3h", "4h", "6h", "8h", "12h", "1d",
                "1w", "2w", "1M",
            ],
            futures: &[
                "1m", "3m", "5m", "15m", "30m", "1h", "2h", "3h", "4h", "6h", "8h", "12h", "1d",
                "1w", "2w", "1M",
            ],
        }
    }
}
