//! # Kraken Connector
//!
//! Implementation of all core traits for Kraken.
//!
//! ## Core traits
//! - `ExchangeIdentity` - exchange identification
//! - `MarketData` - market data
//! - `Trading` - trading operations
//! - `Account` - account information
//! - `Positions` - 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,
    ExchangeError, ExchangeResult,
    Price, Kline, Ticker, OrderBook,
    SymbolInput,
    Order, OrderSide, OrderType, Balance, AccountInfo,
    Position, FundingRate,
    OrderRequest, CancelRequest, CancelScope,
    BalanceQuery, PositionQuery, PositionModification,
    OrderHistoryFilter, PlaceOrderResponse, FeeInfo,
    AmendRequest, CancelAllResponse, OrderResult,
    MarketDataCapabilities, TradingCapabilities, AccountCapabilities,
};
use crate::core::types::{
    WithdrawRequest, WithdrawResponse, DepositAddress,
    FundsHistoryFilter, FundsRecord, FundsRecordType,
    SubAccountOperation, SubAccountResult,
    UserTrade, UserTradeFilter,
    FundingPayment, FundingFilter, LedgerEntry, LedgerFilter,
    OpenInterest, LongShortRatio, PublicTrade,
};
use crate::core::types::SymbolInfo;
use crate::core::traits::{
    ExchangeIdentity, MarketData, Trading, Account, Positions,
    CancelAll, AmendOrder, BatchOrders, CustodialFunds, SubAccounts,
    FundingHistory, AccountLedger, MarketDataPublic,
};
use crate::core::types::ConnectorStats;
use crate::core::utils::{RuntimeLimiter, RateLimitMonitor, RateLimitPressure};
use crate::core::types::{RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, EndpointWeight, DecayingLimitConfig, OrderbookCapabilities, WsBookChannel, ChecksumInfo, ChecksumAlgorithm};
use crate::core::utils::precision::PrecisionCache;

use super::endpoints::{KrakenUrls, KrakenEndpoint, format_symbol, map_ohlc_interval, map_futures_chart_resolution};
use super::auth::KrakenAuth;
use super::parser::KrakenParser;

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

static KRAKEN_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
    model: LimitModel::Decaying,
    rest_pools: &[] as &[RestLimitPool],
    decaying: Some(DecayingLimitConfig {
        max_counter: 15.0,
        decay_rate_per_sec: 0.33,
        default_cost: 1.0,
    }),
    endpoint_weights: &[] as &[EndpointWeight],
    ws: WsLimits {
        max_connections: Some(150),
        max_subs_per_conn: None,
        max_msg_per_sec: None,
        max_streams_per_conn: None,
    },
};

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

/// Kraken connector
pub struct KrakenConnector {
    /// HTTP client
    http: HttpClient,
    /// Authentication (None for public methods)
    auth: Option<KrakenAuth>,
    /// URLs (mainnet/testnet)
    urls: KrakenUrls,
    /// Testnet mode
    testnet: bool,
    /// REST base URL override for proxy / Path-B routing.
    /// When set, replaces `self.urls.rest_url(account_type)` in every request.
    rest_override: Option<String>,
    /// Runtime rate limiter (Decaying model: max=15, decay=0.33/s)
    limiter: Arc<Mutex<RuntimeLimiter>>,
    /// Pressure monitor
    monitor: Arc<Mutex<RateLimitMonitor>>,
    /// Per-symbol precision cache (populated after get_exchange_info)
    precision: PrecisionCache,
}

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

    /// Create new connector with optional REST base URL override.
    pub async fn new_with_override(credentials: Option<Credentials>, testnet: bool, rest_override: Option<String>) -> ExchangeResult<Self> {
        let urls = if testnet {
            KrakenUrls::TESTNET
        } else {
            KrakenUrls::MAINNET
        };

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

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

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

        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 if needed.
    ///
    /// Uses the Decaying model — cost 1 per request.
    /// Non-essential requests are dropped at >= 90% utilization.
    /// Returns `true` if acquired, `false` if dropped.
    async fn rate_limit_wait(&self, essential: bool) -> bool {
        loop {
            let wait_time = {
                let mut limiter = self.limiter.lock().expect("limiter poisoned");
                let pressure = self.monitor.lock().expect("monitor poisoned").check(&mut limiter);
                if pressure >= RateLimitPressure::Cutoff && !essential {
                    return false;
                }
                if limiter.try_acquire("default", 1) {
                    return true;
                }
                limiter.time_until_ready("default", 1)
            };
            if wait_time > Duration::ZERO {
                tokio::time::sleep(wait_time).await;
            }
        }
    }

    /// GET request
    async fn get(
        &self,
        endpoint: KrakenEndpoint,
        params: HashMap<String, String>,
        account_type: AccountType,
    ) -> ExchangeResult<Value> {
        // GET requests are public market data — non-essential
        if !self.rate_limit_wait(false).await {
            return Err(ExchangeError::RateLimitExceeded {
                retry_after: None,
                message: "Rate limit budget >= 90% used; market data request dropped".to_string(),
            });
        }

        let real_base = self.urls.rest_url(account_type);
        let path = endpoint.path();

        // Build query string
        let query = if params.is_empty() {
            String::new()
        } else {
            let qs: Vec<String> = params.iter()
                .map(|(k, v)| format!("{}={}", k, v))
                .collect();
            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?;
        Ok(response)
    }

    /// POST request (Spot API uses POST for both public and private)
    ///
    /// Note: Kraken expects application/x-www-form-urlencoded, but our HttpClient
    /// always sends JSON. As a workaround, we send form params as query params
    /// since Kraken private endpoints accept parameters in either the body or URL.
    async fn post(
        &self,
        endpoint: KrakenEndpoint,
        params: HashMap<String, String>,
        account_type: AccountType,
    ) -> ExchangeResult<Value> {
        // POST requests are trading operations — always essential
        self.rate_limit_wait(true).await;

        let real_base = self.urls.rest_url(account_type);
        let path = endpoint.path();

        if endpoint.requires_auth() {
            let auth = self.auth.as_ref()
                .ok_or_else(|| ExchangeError::Auth("Authentication required".to_string()))?;

            // Sign request to get headers and form body
            let (headers, _body_str) = auth.sign_request(path, &params);

            // Build URL with path
            let url = assemble_rest_url(self.rest_override.as_deref(), real_base, path, "");

            // Use post_with_params - sends params as query string
            // The signature covers the POST body, but Kraken also accepts params in URL
            self.http.post_with_params(&url, &params, &json!({}), &headers).await
        } else {
            // Public POST endpoints (rare for Kraken)
            let url = assemble_rest_url(self.rest_override.as_deref(), real_base, path, "");
            self.http.post_with_params(&url, &params, &json!({}), &HashMap::new()).await
        }
    }

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

    /// Get all asset pairs information
    pub async fn get_asset_pairs(&self) -> ExchangeResult<Value> {
        self.get(KrakenEndpoint::SpotAssetPairs, HashMap::new(), AccountType::Spot).await
    }

    /// Get WebSocket authentication token
    pub async fn get_ws_token(&self) -> ExchangeResult<String> {
        let response = self.post(
            KrakenEndpoint::SpotWebSocketToken,
            HashMap::new(),
            AccountType::Spot,
        ).await?;

        let result = KrakenParser::extract_result(&response)?;
        result.get("token")
            .and_then(|t| t.as_str())
            .map(String::from)
            .ok_or_else(|| ExchangeError::Parse("Missing WebSocket token".to_string()))
    }

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

    /// Get personal trade history (fills) for spot account.
    ///
    /// `trade_type`: optional filter — `"all"`, `"any position"`, `"closed position"`,
    /// `"closing position"`, `"no position"`.
    /// `start` and `end` are Unix timestamps (seconds).
    pub async fn get_trades_history(
        &self,
        trade_type: Option<&str>,
        start: Option<i64>,
        end: Option<i64>,
        offset: Option<u32>,
    ) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        if let Some(t) = trade_type {
            params.insert("type".to_string(), t.to_string());
        }
        if let Some(s) = start {
            params.insert("start".to_string(), s.to_string());
        }
        if let Some(e) = end {
            params.insert("end".to_string(), e.to_string());
        }
        if let Some(o) = offset {
            params.insert("ofs".to_string(), o.to_string());
        }
        self.post(KrakenEndpoint::TradesHistory, params, AccountType::Spot).await
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // FUTURES DERIVATIVES DATA (public)
    // ═══════════════════════════════════════════════════════════════════════════

    /// Get open interest for all Kraken Futures instruments.
    ///
    /// Calls `GET /derivatives/api/v3/tickers` (Kraken Futures REST) and extracts
    /// `openInterest` for each instrument. Returns the raw JSON array so callers
    /// can parse per-instrument data without an additional round-trip.
    ///
    /// Note: Kraken Spot has no open interest data — this endpoint is Futures-only.
    /// The dedicated `/openinterests` path is undocumented and returns 404 on the
    /// live API. OI is exposed through `GET /derivatives/api/v3/tickers`.
    /// Verified live: `GET /derivatives/api/v3/tickers?symbol=PF_XBTUSD` returns
    /// `{"symbol":"PF_XBTUSD","openInterest":1850.266,...}`.
    ///
    /// `symbol` — optional futures symbol (e.g. `"PF_XBTUSD"`); when None,
    /// returns OI for all active contracts.
    pub async fn get_futures_open_interest(&self, symbol: Option<&str>) -> ExchangeResult<Value> {
        let mut params = HashMap::new();
        if let Some(sym) = symbol {
            params.insert("symbol".to_string(), sym.to_string());
        }
        self.get(
            KrakenEndpoint::FuturesTickers,
            params,
            AccountType::FuturesCross,
        ).await
    }

    // ═══════════════════════════════════════════════════════════════════════════
    // FUTURES CHARTS (mark/index price klines — charts/v1)
    // ═══════════════════════════════════════════════════════════════════════════

    /// GET request to Kraken Futures with a fully-formed path string.
    ///
    /// Used for charts/v1 endpoints where the path contains dynamic segments
    /// (`/api/charts/v1/{tick_type}/{symbol}/{resolution}`).  The base URL is
    /// always `https://futures.kraken.com` regardless of account type.
    async fn get_futures_path(
        &self,
        path: &str,
        params: HashMap<String, String>,
    ) -> ExchangeResult<Value> {
        if !self.rate_limit_wait(false).await {
            return Err(ExchangeError::RateLimitExceeded {
                retry_after: None,
                message: "Rate limit budget >= 90% used; futures charts request dropped".to_string(),
            });
        }

        let real_base = self.urls.futures_rest;
        let query = if params.is_empty() {
            String::new()
        } else {
            let qs: Vec<String> = 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);
        self.http.get(&url, &HashMap::new()).await
    }

    /// Fetch mark or index price klines from Kraken Futures charts/v1.
    ///
    /// `tick_type`: `"mark"` or `"spot"` (spot = index price).
    /// `symbol`: Futures native symbol, e.g. `"PF_XBTUSD"`.
    /// `interval`: canonical interval string, e.g. `"1h"`.
    /// `from` / `to`: Unix timestamps in seconds (optional).
    async fn get_futures_chart_klines(
        &self,
        tick_type: &str,
        symbol: &str,
        interval: &str,
        from: Option<i64>,
        to: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let resolution = map_futures_chart_resolution(interval);
        let path = format!("/api/charts/v1/{}/{}/{}", tick_type, symbol, resolution);

        // charts/v1 with only `to` returns candles from genesis (oldest-first,
        // default cap) — NOT the window ending at `to`. Bound `from` to a recent
        // window (≈1000 bars of `interval`) so the result actually covers [.., to].
        let from = from.or_else(|| {
            to.map(|t| t - (map_ohlc_interval(interval) as i64) * 60 * 1000)
        });

        let mut params = HashMap::new();
        if let Some(f) = from {
            params.insert("from".to_string(), f.to_string());
        }
        if let Some(t) = to {
            params.insert("to".to_string(), t.to_string());
        }

        let response = self.get_futures_path(&path, params).await?;
        KrakenParser::parse_charts_candles(&response)
    }

    /// Fetch a Kraken Futures analytics series.
    ///
    /// `GET https://futures.kraken.com/api/charts/v1/analytics/{symbol}/{analytics_type}`
    /// `analytics_type` (live-verified): `"open-interest"`, `"long-short-info"`.
    /// `interval_secs`: bucket width in seconds. `from`/`to`: Unix seconds.
    /// Returns `{result:{timestamp:[secs], data:..}}` for a typed parser.
    async fn get_futures_analytics(
        &self,
        symbol: &str,
        analytics_type: &str,
        from: Option<i64>,
        to: Option<i64>,
        interval_secs: i64,
    ) -> ExchangeResult<serde_json::Value> {
        let path = format!("/api/charts/v1/analytics/{}/{}", symbol, analytics_type);
        let mut params = HashMap::new();
        params.insert("interval".to_string(), interval_secs.to_string());
        if let Some(f) = from {
            params.insert("from".to_string(), f.to_string());
        }
        if let Some(t) = to {
            params.insert("to".to_string(), t.to_string());
        }
        self.get_futures_path(&path, params).await
    }
}

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

impl ExchangeIdentity for KrakenConnector {
    fn exchange_id(&self) -> ExchangeId {
        ExchangeId::Kraken
    }

    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 rate_limit_capabilities(&self) -> RateLimitCapabilities {
        KRAKEN_RATE_CAPS
    }

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

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

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

    fn orderbook_capabilities(&self, account_type: AccountType) -> OrderbookCapabilities {
        static SPOT_CHANNELS: &[WsBookChannel] = &[
            WsBookChannel::delta("book", None, None),
        ];
        match account_type {
            AccountType::Spot => OrderbookCapabilities {
                ws_depths: &[10, 25, 100, 500, 1000],
                ws_default_depth: Some(10),
                rest_max_depth: Some(500),
                rest_depth_values: &[],
                supports_snapshot: true,
                supports_delta: true,
                update_speeds_ms: &[],
                default_speed_ms: None,
                ws_channels: SPOT_CHANNELS,
                checksum: Some(ChecksumInfo {
                    algorithm: ChecksumAlgorithm::Crc32KrakenFormat,
                    levels_per_side: 10,
                    opt_in: false,
                }),
                has_sequence: false,
                has_prev_sequence: false,
                supports_aggregation: false,
                aggregation_levels: &[],
            },
            _ => OrderbookCapabilities {
                ws_depths: &[],
                ws_default_depth: None,
                rest_max_depth: None,
                rest_depth_values: &[],
                supports_snapshot: true,
                supports_delta: true,
                update_speeds_ms: &[],
                default_speed_ms: None,
                ws_channels: &[],
                checksum: None,
                has_sequence: true,
                has_prev_sequence: false,
                supports_aggregation: false,
                aggregation_levels: &[],
            },
        }
    }
}

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

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

        let response = self.get(KrakenEndpoint::SpotTicker, params, account_type).await?;

        // Response may use full ISO format (XXBTZUSD); try raw symbol first, then full format
        KrakenParser::parse_price(&response, &symbol)
            .or_else(|_| {
                let full_format = Self::to_full_format(&symbol);
                KrakenParser::parse_price(&response, &full_format)
            })
    }

    async fn get_orderbook(
        &self,
        symbol: SymbolInput<'_>,
        depth: Option<u16>,
        account_type: AccountType,
    ) -> ExchangeResult<OrderBook> {
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let mut params = HashMap::new();
        params.insert("pair".to_string(), symbol.to_string());
        if let Some(d) = depth {
            params.insert("count".to_string(), d.to_string());
        }

        let response = self.get(KrakenEndpoint::SpotOrderbook, params, account_type).await?;

        KrakenParser::parse_orderbook(&response, &symbol)
            .or_else(|_| {
                let full_format = Self::to_full_format(&symbol);
                KrakenParser::parse_orderbook(&response, &full_format)
            })
    }

    /// `GET /0/public/OHLC` — genuinely has NO backward pagination.
    ///
    /// Wave 2 investigation (2026-07-08): live-probed `since` across every
    /// value tried — 1 day ago, 30 days ago, `since=0` (epoch) — and every
    /// call returned the IDENTICAL most-recent ~720/721 candles (confirmed
    /// both at `interval=1` and `interval=1440`). There is no `count`/
    /// `limit` param either (undocumented, and live-probed to have no
    /// effect). This is a hard venue-side ceiling, not a bug in this
    /// wiring: Kraken's own docs describe `since` as a "return committed
    /// OHLC data since given id" cursor, but the live behavior is that the
    /// endpoint always serves its fixed recent window regardless of the
    /// value passed. `_limit` and `_end_time` are left unused (documented,
    /// not silently dropped) because there is nothing on the wire for them
    /// to control — wiring them would not change the request in any way.
    /// `trade_history_capabilities().kline_backpage = false` reflects this.
    async fn get_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        _limit: Option<u16>,
        account_type: AccountType,
        _end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let mut params = HashMap::new();
        params.insert("pair".to_string(), symbol.to_string());
        params.insert("interval".to_string(), map_ohlc_interval(interval).to_string());

        let response = self.get(KrakenEndpoint::SpotOHLC, params, account_type).await?;

        KrakenParser::parse_klines(&response, &symbol)
            .or_else(|_| {
                let full_format = Self::to_full_format(&symbol);
                KrakenParser::parse_klines(&response, &full_format)
            })
    }

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

        if matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated) {
            let mut params = HashMap::new();
            params.insert("symbol".to_string(), symbol.to_string());
            let response = self.get(KrakenEndpoint::FuturesTickers, params, account_type).await?;
            return KrakenParser::parse_futures_ticker(&response, &symbol);
        }

        let mut params = HashMap::new();
        params.insert("pair".to_string(), symbol.to_string());

        let response = self.get(KrakenEndpoint::SpotTicker, params, account_type).await?;

        KrakenParser::parse_ticker(&response, &symbol)
            .or_else(|_| {
                let full_format = Self::to_full_format(&symbol);
                KrakenParser::parse_ticker(&response, &full_format)
            })
    }

    async fn ping(&self) -> ExchangeResult<()> {
        let response = self.get(KrakenEndpoint::ServerTime, HashMap::new(), AccountType::Spot).await?;
        KrakenParser::extract_result(&response)?;
        Ok(())
    }

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

    fn market_data_capabilities(&self, _account_type: AccountType) -> MarketDataCapabilities {
        MarketDataCapabilities {
            has_ping: true,
            has_price: true,
            has_ticker: true,
            has_orderbook: true,
            has_klines: true,
            has_exchange_info: true,
            // Kraken Spot recent-trades: GET /0/public/Trades (Futures: no public REST endpoint).
            has_recent_trades: true,
            // Kraken OHLC intervals (integer minutes): 1, 5, 15, 30, 60, 240, 1440, 10080, 21600
            supported_intervals: &["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "15d"],
            // Kraken returns up to 720 candles per OHLC request.
            max_kline_limit: Some(720),
            // Kraken WebSocket v2: ohlc channel for klines.
            has_ws_klines: true,
            // Kraken WebSocket v2: trade channel.
            has_ws_trades: true,
            // Kraken WebSocket v2: book channel for orderbook.
            has_ws_orderbook: true,
            // Kraken WebSocket v2: ticker channel.
            has_ws_ticker: 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 KrakenConnector {
    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 = format_symbol(&symbol.base, &symbol.quote, account_type);
        let side_str = match side { OrderSide::Buy => "buy", OrderSide::Sell => "sell" };
        let sym = &formatted;

        // Futures endpoint selection
        let endpoint = match account_type {
            AccountType::Spot | AccountType::Margin => KrakenEndpoint::SpotAddOrder,
            _ => KrakenEndpoint::FuturesSendOrder,
        };

        let (mut params, order_type_out, price_out, stop_price_out, tif_out) = match req.order_type {
            OrderType::Market => {
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "market".to_string());
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                (p, OrderType::Market, None, None, crate::core::TimeInForce::Gtc)
            }
            OrderType::Limit { price } => {
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "limit".to_string());
                p.insert("price".to_string(), self.precision.price(sym, price));
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                (p, OrderType::Limit { price }, Some(price), None, crate::core::TimeInForce::Gtc)
            }
            OrderType::PostOnly { price } => {
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "limit".to_string());
                p.insert("price".to_string(), self.precision.price(sym, price));
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                p.insert("oflags".to_string(), "post".to_string());
                (p, OrderType::PostOnly { price }, Some(price), None, crate::core::TimeInForce::Gtc)
            }
            OrderType::Ioc { price } => {
                let px_val = price.unwrap_or(0.0);
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "limit".to_string());
                p.insert("price".to_string(), self.precision.price(sym, px_val));
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                p.insert("timeinforce".to_string(), "IOC".to_string());
                (p, OrderType::Ioc { price }, price, None, crate::core::TimeInForce::Ioc)
            }
            OrderType::Fok { price } => {
                // Kraken does not natively support FOK; treat as IOC
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "limit".to_string());
                p.insert("price".to_string(), self.precision.price(sym, price));
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                p.insert("timeinforce".to_string(), "IOC".to_string());
                (p, OrderType::Fok { price }, Some(price), None, crate::core::TimeInForce::Fok)
            }
            OrderType::StopMarket { stop_price } => {
                // Kraken: ordertype=stop-loss, price=stop trigger
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "stop-loss".to_string());
                p.insert("price".to_string(), self.precision.price(sym, stop_price));
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                (p, OrderType::StopMarket { stop_price }, None, Some(stop_price), crate::core::TimeInForce::Gtc)
            }
            OrderType::StopLimit { stop_price, limit_price } => {
                // Kraken: ordertype=stop-loss-limit, price=stop trigger, price2=limit price
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "stop-loss-limit".to_string());
                p.insert("price".to_string(), self.precision.price(sym, stop_price));
                p.insert("price2".to_string(), self.precision.price(sym, limit_price));
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                (p, OrderType::StopLimit { stop_price, limit_price }, Some(limit_price), Some(stop_price), crate::core::TimeInForce::Gtc)
            }
            OrderType::Gtd { price, expire_time } => {
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "limit".to_string());
                p.insert("price".to_string(), self.precision.price(sym, price));
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                // Kraken GTD: timeinforce=GTD + expiretm = Unix timestamp or +<seconds>
                p.insert("timeinforce".to_string(), "GTD".to_string());
                p.insert("expiretm".to_string(), (expire_time / 1000).to_string());
                (p, OrderType::Gtd { price, expire_time }, Some(price), None, crate::core::TimeInForce::Gtd)
            }
            OrderType::ReduceOnly { price } => {
                // Kraken Futures: reduceOnly flag
                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "ReduceOnly not supported for spot on Kraken".to_string()
                        ));
                    }
                    _ => {}
                }
                let ord_type = if price.is_some() { "lmt" } else { "mkt" };
                let mut p = HashMap::new();
                p.insert("symbol".to_string(), formatted.clone());
                p.insert("side".to_string(), side_str.to_string());
                p.insert("orderType".to_string(), ord_type.to_string());
                p.insert("size".to_string(), self.precision.qty(sym, quantity));
                p.insert("reduceOnly".to_string(), "true".to_string());
                if let Some(px) = price {
                    p.insert("limitPrice".to_string(), self.precision.price(sym, px));
                }
                (p, OrderType::ReduceOnly { price }, price, None, crate::core::TimeInForce::Gtc)
            }
            OrderType::Iceberg { price, display_quantity } => {
                // Kraken native iceberg: ordertype=iceberg, displayvol=visible slice size
                let mut p = HashMap::new();
                p.insert("pair".to_string(), formatted.clone());
                p.insert("type".to_string(), side_str.to_string());
                p.insert("ordertype".to_string(), "iceberg".to_string());
                p.insert("price".to_string(), self.precision.price(sym, price));
                p.insert("volume".to_string(), self.precision.qty(sym, quantity));
                p.insert("displayvol".to_string(), self.precision.qty(sym, display_quantity));
                (p, OrderType::Iceberg { price, display_quantity }, Some(price), None, crate::core::TimeInForce::Gtc)
            }
            OrderType::TrailingStop { .. } | OrderType::Oco { .. } | OrderType::Bracket { .. }
            | OrderType::Twap { .. }
            | OrderType::Oto { .. } | OrderType::ConditionalPlan { .. } | OrderType::DcaRecurring { .. } => {
                return Err(ExchangeError::NotImplemented(
                    format!("{:?} order type not supported on {:?}", req.order_type, self.exchange_id())
                ));
            }
        };

        // For futures, rename params to Kraken Futures API format
        if matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated) {
            // Futures API uses different param names
            if let Some(pair) = params.remove("pair") {
                params.insert("symbol".to_string(), pair);
            }
            if let Some(t) = params.remove("type") {
                params.insert("side".to_string(), t);
            }
            if let Some(ot) = params.remove("ordertype") {
                let futures_type = match ot.as_str() {
                    "market" => "mkt",
                    "limit" => "lmt",
                    "stop-loss" => "stp",
                    _ => "lmt",
                };
                params.insert("orderType".to_string(), futures_type.to_string());
            }
            if let Some(vol) = params.remove("volume") {
                params.insert("size".to_string(), vol);
            }
            if let Some(px) = params.remove("price") {
                params.insert("limitPrice".to_string(), px);
            }
        }

        if let Some(ref cl_id) = req.client_order_id {
            params.insert("cl_ord_id".to_string(), cl_id.clone());
        }

        let response = self.post(endpoint, params, account_type).await?;
        let order_id = KrakenParser::parse_order_id(&response)?;

        Ok(PlaceOrderResponse::Simple(Order {
            id: order_id,
            client_order_id: req.client_order_id,
            symbol: Some(symbol.to_string()),
            side,
            order_type: order_type_out,
            status: crate::core::OrderStatus::New,
            price: price_out,
            stop_price: stop_price_out,
            quantity,
            filled_quantity: 0.0,
            average_price: None,
            commission: None,
            commission_asset: None,
            created_at: crate::core::timestamp_millis() as i64,
            updated_at: None,
            time_in_force: tif_out,
        }))
    }

    async fn get_order_history(
        &self,
        filter: OrderHistoryFilter,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<Order>> {
        // Kraken Spot: POST /0/private/ClosedOrders
        // Kraken Futures: GET /derivatives/api/v3/fills
        match account_type {
            AccountType::Spot | AccountType::Margin => {
                let mut params = HashMap::new();

                if let Some(start) = filter.start_time {
                    params.insert("start".to_string(), (start / 1000).to_string());
                }
                if let Some(end) = filter.end_time {
                    params.insert("end".to_string(), (end / 1000).to_string());
                }

                let response = self.post(KrakenEndpoint::SpotClosedOrders, params, account_type).await?;
                KrakenParser::parse_closed_orders(&response)
            }
            _ => {
                // Futures: GET /derivatives/api/v3/fills
                let mut params = HashMap::new();
                if let Some(start) = filter.start_time {
                    params.insert("lastFillTime".to_string(), start.to_string());
                }

                let response = self.get(KrakenEndpoint::FuturesHistory, params, account_type).await?;
                KrakenParser::parse_futures_fills(&response)
            }
        }
    }

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

                let mut params = HashMap::new();
                params.insert("txid".to_string(), order_id.to_string());

                let response = self.post(KrakenEndpoint::SpotCancelOrder, params, account_type).await?;
                KrakenParser::extract_result(&response)?;

                Ok(Order {
                    id: order_id.to_string(),
                    client_order_id: None,
                    symbol: Some(symbol.to_string()),
                    side: OrderSide::Buy,
                    order_type: OrderType::Limit { price: 0.0 },
                    status: crate::core::OrderStatus::Canceled,
                    price: None,
                    stop_price: None,
                    quantity: 0.0,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: 0,
                    updated_at: Some(crate::core::timestamp_millis() as i64),
                    time_in_force: crate::core::TimeInForce::Gtc,
                })
            }
            CancelScope::All { ref symbol } => {
                let account_type = req.account_type;
                let mut params = HashMap::new();
                // For futures, optional symbol filter
                if let Some(sym) = symbol {
                    if matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated) {
                        params.insert("symbol".to_string(),
                            format_symbol(&sym.base, &sym.quote, account_type));
                    }
                }
                let cancel_all_endpoint = match account_type {
                    AccountType::Spot | AccountType::Margin => KrakenEndpoint::SpotCancelOrder,
                    _ => KrakenEndpoint::FuturesCancelOrder,
                };
                let response = self.post(cancel_all_endpoint, params, account_type).await?;
                let _ = response;
                let sym_str = symbol.as_ref().map(|s| s.to_string());
                Ok(Order {
                    id: format!("cancel_all_{}", crate::core::timestamp_millis()),
                    client_order_id: None,
                    symbol: sym_str,
                    side: OrderSide::Buy,
                    order_type: OrderType::Market,
                    status: crate::core::OrderStatus::Canceled,
                    price: None,
                    stop_price: None,
                    quantity: 0.0,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: 0,
                    updated_at: Some(crate::core::timestamp_millis() as i64),
                    time_in_force: crate::core::TimeInForce::Gtc,
                })
            }
            CancelScope::BySymbol { ref symbol } => {
                let account_type = req.account_type;
                let mut params = HashMap::new();
                if matches!(account_type, AccountType::FuturesCross | AccountType::FuturesIsolated) {
                    params.insert("symbol".to_string(),
                        format_symbol(&symbol.base, &symbol.quote, account_type));
                }
                let cancel_all_endpoint = match account_type {
                    AccountType::Spot | AccountType::Margin => KrakenEndpoint::SpotCancelOrder,
                    _ => KrakenEndpoint::FuturesCancelOrder,
                };
                let response = self.post(cancel_all_endpoint, params, account_type).await?;
                let _ = response;
                Ok(Order {
                    id: format!("cancel_all_{}", crate::core::timestamp_millis()),
                    client_order_id: None,
                    symbol: Some(symbol.to_string()),
                    side: OrderSide::Buy,
                    order_type: OrderType::Market,
                    status: crate::core::OrderStatus::Canceled,
                    price: None,
                    stop_price: None,
                    quantity: 0.0,
                    filled_quantity: 0.0,
                    average_price: None,
                    commission: None,
                    commission_asset: None,
                    created_at: 0,
                    updated_at: Some(crate::core::timestamp_millis() as i64),
                    time_in_force: crate::core::TimeInForce::Gtc,
                })
            }
            CancelScope::Batch { ref order_ids } => {
                let symbol = req.symbol.as_ref()
                    .ok_or_else(|| ExchangeError::InvalidRequest("Symbol required for batch cancel".into()))?
                    .clone();
                let account_type = req.account_type;

                // Kraken Futures supports batch cancel: POST /derivatives/api/v3/cancelallorders
                // For spot, there's no native batch; return NotImplemented
                match account_type {
                    AccountType::Spot | AccountType::Margin => {
                        return Err(ExchangeError::NotImplemented(
                            "Kraken Spot does not support batch cancel. Cancel orders individually.".to_string()
                        ));
                    }
                    _ => {}
                }

                // Futures batch: cancel each by sending multiple cancel requests (no single endpoint)
                // Per non-composition rule, return NotImplemented for batch
                let _ = (order_ids, symbol);
                Err(ExchangeError::NotImplemented(
                    "Kraken Futures batch cancel requires individual cancels. Use CancelScope::Single.".to_string()
                ))
            }
            CancelScope::ByLabel(_)
            | CancelScope::ByCurrencyKind { .. }
            | CancelScope::ScheduledAt(_) => Err(ExchangeError::NotImplemented(
                "Kraken does not support this cancel scope".to_string()
            )),
        }
    }

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

        let mut params = HashMap::new();
        params.insert("txid".to_string(), order_id.to_string());

        let response = self.post(KrakenEndpoint::SpotGetOrder, params, account_type).await?;
        KrakenParser::parse_order(&response, order_id)
    
    }

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

        let params = HashMap::new();
        let response = self.post(KrakenEndpoint::SpotOpenOrders, params, account_type).await?;
        KrakenParser::parse_open_orders(&response)

    }

    /// Get personal trade fills from Kraken.
    ///
    /// Uses `POST /0/private/TradesHistory` for Spot/Margin.
    /// Futures fills use `GET /derivatives/api/v3/fills` — returns
    /// `NotImplemented` since the Futures fills endpoint returns
    /// orders (not `UserTrade` format) and is already covered by
    /// `get_order_history`.
    ///
    /// Offset-based pagination: Kraken returns up to 50 records per request.
    /// When `filter.limit` exceeds 50, multiple pages are fetched automatically
    /// until the requested limit is reached or no more records exist.
    async fn get_user_trades(
        &self,
        filter: UserTradeFilter,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<UserTrade>> {
        match account_type {
            AccountType::FuturesCross | AccountType::FuturesIsolated => {
                return Err(ExchangeError::NotImplemented(
                    "get_user_trades is not supported for Kraken Futures (use get_order_history)".to_string(),
                ));
            }
            _ => {}
        }

        // Kraken returns up to 50 trades per page; paginate to satisfy `limit`.
        let page_size: u32 = 50;
        let max_trades = filter.limit.unwrap_or(page_size);

        // Convert ms timestamps to Unix seconds for Kraken API.
        let start_secs = filter.start_time.map(|ms| ms / 1000);
        let end_secs = filter.end_time.map(|ms| ms / 1000);

        let mut all_trades: Vec<UserTrade> = Vec::new();
        let mut offset: u32 = 0;

        loop {
            let response = self.get_trades_history(
                None,
                start_secs.map(|s| s as i64),
                end_secs.map(|s| s as i64),
                if offset > 0 { Some(offset) } else { None },
            ).await?;

            let mut page = KrakenParser::parse_trades_history(&response)?;

            // Apply order_id filter (Kraken has no server-side filter for this).
            if let Some(ref oid) = filter.order_id {
                page.retain(|t| &t.order_id == oid);
            }

            // Apply symbol filter (Kraken has no server-side symbol filter for TradesHistory).
            if let Some(ref sym) = filter.symbol {
                let sym_upper = sym.to_uppercase();
                page.retain(|t| t.symbol.to_uppercase().contains(&sym_upper));
            }

            let page_len = page.len() as u32;
            all_trades.extend(page);

            // Stop if we have enough records or this page was smaller than a full page.
            if all_trades.len() as u32 >= max_trades || page_len < page_size {
                break;
            }

            offset += page_size;
        }

        // Truncate to requested limit.
        all_trades.truncate(max_trades as usize);

        Ok(all_trades)
    }

    fn trading_capabilities(&self, account_type: AccountType) -> TradingCapabilities {
        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
        TradingCapabilities {
            has_market_order: true,
            has_limit_order: true,
            has_stop_market: true,  // stop-loss (market trigger) implemented
            has_stop_limit: true,   // stop-loss-limit implemented
            // TrailingStop / OCO / Bracket all return NotImplemented in place_order.
            has_trailing_stop: false,
            has_bracket: false,
            has_oco: false,
            // AmendOrder impl exists for both Spot (EditOrder) and Futures (editorder).
            has_amend: true,
            // Futures: native batch via /batchorder (max 10). Spot: no batch endpoint.
            has_batch: is_futures,
            max_batch_size: if is_futures { Some(10) } else { None },
            // CancelAll impl exists for both Spot (/CancelAll) and Futures (/cancelallorders).
            has_cancel_all: true,
            // get_user_trades: Spot only via TradesHistory. Futures returns NotImplemented.
            has_user_trades: !is_futures,
            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 KrakenConnector {
    async fn get_balance(&self, query: BalanceQuery) -> ExchangeResult<Vec<Balance>> {
        let _asset = query.asset.clone();
        let account_type = query.account_type;

        let params = HashMap::new();
        let response = self.post(KrakenEndpoint::SpotBalance, params, account_type).await?;
        KrakenParser::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.16, // Kraken default maker fee (varies by tier)
            taker_commission: 0.26, // Kraken default taker fee
            balances,
        })
    }

    async fn get_fees(&self, symbol: Option<&str>) -> ExchangeResult<FeeInfo> {
        // Kraken: POST /0/private/TradeVolume returns fee schedule
        let account_type = AccountType::Spot;
        let mut params = HashMap::new();

        if let Some(sym) = symbol {
            let parts: Vec<&str> = sym.split('/').collect();
            let formatted = if parts.len() == 2 {
                format_symbol(parts[0], parts[1], account_type)
            } else {
                sym.to_string()
            };
            params.insert("pair".to_string(), formatted);
        }

        let response = self.post(KrakenEndpoint::SpotTradeBalance, params, account_type).await?;
        let result = KrakenParser::extract_result(&response)?;

        // Default Kraken fees (Starter tier: maker 0.16%, taker 0.26%)
        let maker_rate = result.get("fee")
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse::<f64>().ok())
            .map(|v| v / 100.0)
            .unwrap_or(0.0016);
        let taker_rate = maker_rate; // TradeBalance returns taker fee

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

    fn account_capabilities(&self, account_type: AccountType) -> AccountCapabilities {
        let is_futures = !matches!(account_type, AccountType::Spot | AccountType::Margin);
        AccountCapabilities {
            has_balances: true,
            has_account_info: true,
            has_fees: true,
            // No AccountTransfers trait implemented for Kraken.
            has_transfers: false,
            // SubAccounts: List and Transfer work for both. Create/GetBalance always NotImplemented.
            has_sub_accounts: true,
            // CustodialFunds endpoints are Spot-only (/DepositAddresses, /Withdraw, etc.).
            has_deposit_withdraw: !is_futures,
            // No dedicated MarginTrading trait implemented.
            has_margin: false,
            // No EarnStaking trait implemented.
            has_earn_staking: false,
            // FundingHistory uses /Ledgers (type=rollover) — Spot endpoint only.
            has_funding_history: !is_futures,
            // AccountLedger uses /Ledgers — Spot endpoint only.
            has_ledger: !is_futures,
            // No ConvertSwap trait implemented.
            has_convert: false,
            // Kraken connector is spot/margin only — no futures positions.
            has_positions: false,
        }
    }
}

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

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

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

        let response = self.get(
            KrakenEndpoint::FuturesOpenPositions,
            HashMap::new(),
            account_type,
        ).await?;

        KrakenParser::parse_futures_positions(&response)
    
    }

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

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

        let formatted = format_symbol(&symbol.base, &symbol.quote, account_type);

        let mut params = HashMap::new();
        params.insert("symbol".to_string(), formatted.clone());

        let response = self.get(
            KrakenEndpoint::FuturesHistoricalFunding,
            params,
            account_type,
        ).await?;

        KrakenParser::parse_funding_rate(&response)
    
    }

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

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

                let formatted = format_symbol(&symbol.base, &symbol.quote, account_type);

                let mut params = HashMap::new();
                params.insert("symbol".to_string(), formatted);
                params.insert("maxLeverage".to_string(), leverage.to_string());

                let response = self.post(KrakenEndpoint::FuturesSetLeverage, params, account_type).await?;
                KrakenParser::extract_futures_data(&response)?;
                Ok(())
            }
            PositionModification::ClosePosition { ref symbol, account_type } => {
                let symbol = symbol.clone();

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

                let formatted = format_symbol(&symbol.base, &symbol.quote, account_type);

                let mut params = HashMap::new();
                params.insert("symbol".to_string(), formatted);
                params.insert("orderType".to_string(), "mkt".to_string());
                // Kraken Futures: send order with reduceOnly to close
                params.insert("reduceOnly".to_string(), "true".to_string());
                // Side will be auto-determined; we send a nominal "buy" which gets overridden by reduceOnly
                params.insert("side".to_string(), "buy".to_string());
                params.insert("size".to_string(), "0".to_string()); // 0 = entire position for some exchanges

                let response = self.post(KrakenEndpoint::FuturesSendOrder, params, account_type).await?;
                KrakenParser::extract_futures_data(&response)?;
                Ok(())
            }
            PositionModification::SetMarginMode { .. }
            | PositionModification::AddMargin { .. }
            | PositionModification::RemoveMargin { .. }
            | PositionModification::SetTpSl { .. }
            | PositionModification::SwitchPositionMode { .. }
            | PositionModification::MovePositions { .. } => {
                Err(ExchangeError::NotImplemented(
                    "This position modification is not supported on Kraken".to_string()
                ))
            }
        }
    }
}

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

/// Cancel all open orders across all symbols.
///
/// - Spot:    `POST /0/private/CancelAll`
/// - Futures: `POST /derivatives/api/v3/cancelallorders` (optionally filtered by symbol)
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CancelAll for KrakenConnector {
    async fn cancel_all_orders(
        &self,
        scope: CancelScope,
        account_type: AccountType,
    ) -> ExchangeResult<CancelAllResponse> {
        let symbol = match &scope {
            CancelScope::All { symbol } => symbol.clone(),
            CancelScope::BySymbol { symbol } => Some(symbol.clone()),
            _ => {
                return Err(ExchangeError::InvalidRequest(
                    "cancel_all_orders only accepts All or BySymbol scope".to_string()
                ));
            }
        };

        match account_type {
            AccountType::Spot | AccountType::Margin => {
                // Spot CancelAll does not support per-symbol filtering
                let response = self.post(KrakenEndpoint::SpotCancelAll, HashMap::new(), account_type).await?;
                KrakenParser::parse_cancel_all_response(&response)
            }
            _ => {
                // Futures: optional symbol filter
                let mut params = HashMap::new();
                if let Some(sym) = symbol {
                    params.insert(
                        "symbol".to_string(),
                        format_symbol(&sym.base, &sym.quote, account_type),
                    );
                }
                let response = self.post(KrakenEndpoint::FuturesCancelOrder, params, account_type).await?;
                KrakenParser::parse_futures_cancel_all_response(&response)
            }
        }
    }
}

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

/// Amend a live order in-place.
///
/// - Spot:    `POST /0/private/EditOrder`
/// - Futures: `POST /derivatives/api/v3/editorder`
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AmendOrder for KrakenConnector {
    async fn amend_order(&self, req: AmendRequest) -> ExchangeResult<Order> {
        if req.fields.price.is_none() && req.fields.quantity.is_none() {
            return Err(ExchangeError::InvalidRequest(
                "At least one of price or quantity must be provided for amend".to_string()
            ));
        }

        let account_type = req.account_type;
        let formatted = format_symbol(&req.symbol.base, &req.symbol.quote, account_type);
        let symbol_str = req.symbol.to_string();

        match account_type {
            AccountType::Spot | AccountType::Margin => {
                // Kraken Spot EditOrder: POST /0/private/EditOrder
                let mut params = HashMap::new();
                params.insert("txid".to_string(), req.order_id.clone());
                params.insert("pair".to_string(), formatted.clone());

                if let Some(price) = req.fields.price {
                    params.insert("price".to_string(), self.precision.price(&formatted, price));
                }
                if let Some(qty) = req.fields.quantity {
                    params.insert("volume".to_string(), self.precision.qty(&formatted, qty));
                }

                let response = self.post(KrakenEndpoint::SpotEditOrder, params, account_type).await?;
                KrakenParser::parse_amend_spot_order(&response, &symbol_str)
            }
            _ => {
                // Kraken Futures editorder
                let mut params = HashMap::new();
                params.insert("orderId".to_string(), req.order_id.clone());
                params.insert("symbol".to_string(), formatted.clone());

                if let Some(price) = req.fields.price {
                    params.insert("limitPrice".to_string(), self.precision.price(&formatted, price));
                }
                if let Some(qty) = req.fields.quantity {
                    params.insert("size".to_string(), self.precision.qty(&formatted, qty));
                }

                let response = self.post(KrakenEndpoint::FuturesEditOrder, params, account_type).await?;
                KrakenParser::parse_amend_futures_order(&response, &symbol_str)
            }
        }
    }
}

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

/// Native batch order placement (Futures only).
///
/// Kraken Futures: `POST /derivatives/api/v3/batchorder` — max 10 orders per batch.
/// Spot does NOT have a native batch placement endpoint.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl BatchOrders for KrakenConnector {
    async fn place_orders_batch(
        &self,
        orders: Vec<OrderRequest>,
    ) -> ExchangeResult<Vec<OrderResult>> {
        if orders.is_empty() {
            return Ok(vec![]);
        }

        let account_type = orders[0].account_type;

        match account_type {
            AccountType::Spot | AccountType::Margin => {
                return Err(ExchangeError::NotImplemented(
                    "Batch orders not supported on Kraken Spot (futures only)".to_string()
                ));
            }
            _ => {}
        }

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

        // Kraken Futures batchorder: POST with JSON body containing orders array
        let batch_json: Vec<serde_json::Value> = orders.iter().map(|req| {
            let formatted = format_symbol(&req.symbol.base, &req.symbol.quote, account_type);
            let side_str = match req.side { OrderSide::Buy => "buy", OrderSide::Sell => "sell" };

            let mut obj = json!({
                "order": "send",
                "symbol": formatted,
                "side": side_str,
                "size": req.quantity as i64,
            });
            match req.order_type {
                OrderType::Market => {
                    obj["orderType"] = json!("mkt");
                }
                OrderType::Limit { price } => {
                    obj["orderType"] = json!("lmt");
                    obj["limitPrice"] = json!(self.precision.price(&formatted, price));
                }
                _ => {
                    obj["orderType"] = json!("mkt");
                }
            }
            if req.reduce_only {
                obj["reduceOnly"] = json!(true);
            }
            if let Some(ref cid) = req.client_order_id {
                obj["cl_ord_id"] = json!(cid);
            }
            obj
        }).collect();

        let mut params = HashMap::new();
        let batch_str = serde_json::to_string(&batch_json)
            .map_err(|e| ExchangeError::Parse(format!("Failed to serialize batch orders: {}", e)))?;
        params.insert("json".to_string(), batch_str);

        let response = self.post(KrakenEndpoint::FuturesBatchOrder, params, account_type).await?;
        KrakenParser::parse_batch_orders_response(&response)
    }

    async fn cancel_orders_batch(
        &self,
        order_ids: Vec<String>,
        _symbol: Option<&str>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<OrderResult>> {
        match account_type {
            AccountType::Spot | AccountType::Margin => {
                return Err(ExchangeError::NotImplemented(
                    "Batch cancel not supported on Kraken Spot".to_string()
                ));
            }
            _ => {}
        }

        // Futures batchorder with cancel operations
        let cancel_json: Vec<serde_json::Value> = order_ids.iter().map(|id| {
            json!({
                "order": "cancel",
                "order_id": id,
            })
        }).collect();

        let mut params = HashMap::new();
        let batch_str = serde_json::to_string(&cancel_json)
            .map_err(|e| ExchangeError::Parse(format!("Failed to serialize cancel batch: {}", e)))?;
        params.insert("json".to_string(), batch_str);

        let response = self.post(KrakenEndpoint::FuturesBatchOrder, params, account_type).await?;
        KrakenParser::parse_batch_orders_response(&response)
    }

    fn max_batch_place_size(&self) -> usize {
        10 // Kraken Futures batchorder limit
    }

    fn max_batch_cancel_size(&self) -> usize {
        10 // Kraken Futures batchorder limit
    }
}

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

/// Deposit and withdrawal management for Kraken.
///
/// - Deposit address: `POST /0/private/DepositAddresses`
/// - Withdraw:        `POST /0/private/Withdraw`
/// - Deposit history: `POST /0/private/DepositStatus`
/// - Withdrawal hist: `POST /0/private/WithdrawStatus`
///
/// Note: Kraken asset names use internal format — XXBT for BTC, ZUSD for USD.
/// This implementation maps common tickers to Kraken's format.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl CustodialFunds for KrakenConnector {
    async fn get_deposit_address(
        &self,
        asset: &str,
        network: Option<&str>,
    ) -> ExchangeResult<DepositAddress> {
        // Map common asset names to Kraken internal format
        let kraken_asset = map_asset_to_kraken(asset);

        let mut params = HashMap::new();
        params.insert("asset".to_string(), kraken_asset.to_string());
        if let Some(method) = network {
            params.insert("method".to_string(), method.to_string());
        }
        // new=true requests a fresh address instead of reusing an existing one
        params.insert("new".to_string(), "false".to_string());

        let response = self.post(
            KrakenEndpoint::SpotDepositAddresses,
            params,
            AccountType::Spot,
        ).await?;

        KrakenParser::parse_deposit_address(&response, asset)
    }

    async fn withdraw(&self, req: WithdrawRequest) -> ExchangeResult<WithdrawResponse> {
        let kraken_asset = map_asset_to_kraken(&req.asset);

        let mut params = HashMap::new();
        params.insert("asset".to_string(), kraken_asset.to_string());
        // Kraken uses a pre-registered withdrawal address "key" (name), not raw address
        // We use the address field as the key name
        params.insert("key".to_string(), req.address.clone());
        params.insert("amount".to_string(), req.amount.to_string());

        let response = self.post(
            KrakenEndpoint::SpotWithdraw,
            params,
            AccountType::Spot,
        ).await?;

        KrakenParser::parse_withdraw_response(&response)
    }

    async fn get_funds_history(
        &self,
        filter: FundsHistoryFilter,
    ) -> ExchangeResult<Vec<FundsRecord>> {
        let asset = filter.asset.as_deref().unwrap_or("");
        let kraken_asset = if asset.is_empty() {
            String::new()
        } else {
            map_asset_to_kraken(asset)
        };

        match filter.record_type {
            FundsRecordType::Deposit => {
                let mut params = HashMap::new();
                if !kraken_asset.is_empty() {
                    params.insert("asset".to_string(), kraken_asset.to_string());
                }
                let response = self.post(
                    KrakenEndpoint::SpotDepositStatus,
                    params,
                    AccountType::Spot,
                ).await?;
                KrakenParser::parse_deposit_history(&response)
            }
            FundsRecordType::Withdrawal => {
                let mut params = HashMap::new();
                if !kraken_asset.is_empty() {
                    params.insert("asset".to_string(), kraken_asset.to_string());
                }
                let response = self.post(
                    KrakenEndpoint::SpotWithdrawStatus,
                    params,
                    AccountType::Spot,
                ).await?;
                KrakenParser::parse_withdrawal_history(&response)
            }
            FundsRecordType::Both => {
                // Fetch both and combine
                let mut deposits_params = HashMap::new();
                let mut withdrawals_params = HashMap::new();
                if !kraken_asset.is_empty() {
                    deposits_params.insert("asset".to_string(), kraken_asset.to_string());
                    withdrawals_params.insert("asset".to_string(), kraken_asset.to_string());
                }
                let dep_response = self.post(
                    KrakenEndpoint::SpotDepositStatus,
                    deposits_params,
                    AccountType::Spot,
                ).await?;
                let wit_response = self.post(
                    KrakenEndpoint::SpotWithdrawStatus,
                    withdrawals_params,
                    AccountType::Spot,
                ).await?;

                let mut records = KrakenParser::parse_deposit_history(&dep_response)?;
                records.extend(KrakenParser::parse_withdrawal_history(&wit_response)?);
                Ok(records)
            }
        }
    }
}

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

/// Sub-account management for Kraken.
///
/// Kraken supports listing sub-accounts and transferring funds between them
/// via the standard private REST API. Creating sub-accounts and querying
/// individual balances are not available through the standard API.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl SubAccounts for KrakenConnector {
    async fn sub_account_operation(
        &self,
        op: SubAccountOperation,
    ) -> ExchangeResult<SubAccountResult> {
        match op {
            SubAccountOperation::List => {
                let response = self.post(
                    KrakenEndpoint::SpotListSubaccounts,
                    HashMap::new(),
                    AccountType::Spot,
                ).await?;
                KrakenParser::parse_list_subaccounts(&response)
            }

            SubAccountOperation::Transfer { sub_account_id, asset, amount, to_sub } => {
                let kraken_asset = map_asset_to_kraken(&asset);
                let mut params = HashMap::new();
                params.insert("asset".to_string(), kraken_asset.to_string());
                params.insert("amount".to_string(), amount.to_string());
                params.insert("subaccount".to_string(), sub_account_id.clone());

                let endpoint = if to_sub {
                    KrakenEndpoint::SpotTransferToSubaccount
                } else {
                    KrakenEndpoint::SpotTransferFromSubaccount
                };

                let response = self.post(endpoint, params, AccountType::Spot).await?;
                KrakenParser::parse_subaccount_transfer(&response)
            }

            SubAccountOperation::Create { .. } => {
                Err(ExchangeError::NotImplemented(
                    "Kraken does not support sub-account creation via standard API".to_string()
                ))
            }

            SubAccountOperation::GetBalance { .. } => {
                Err(ExchangeError::NotImplemented(
                    "Kraken does not support per-sub-account balance queries via standard API".to_string()
                ))
            }
        }
    }
}

// ═══════════════════════════════════════════════════════════════════════════════
// ASSET NAME MAPPING
// ═══════════════════════════════════════════════════════════════════════════════

/// Map common asset ticker to Kraken's internal asset name.
///
/// Kraken uses non-standard names for some assets:
/// - BTC → XXBT
/// - USD → ZUSD
/// - EUR → ZEUR
///
/// For all other assets the ticker is returned as-is (uppercased).
fn map_asset_to_kraken(asset: &str) -> String {
    match asset.to_uppercase().as_str() {
        "BTC" | "XBT" => "XXBT".to_string(),
        "ETH" => "XETH".to_string(),
        "LTC" => "XLTC".to_string(),
        "XRP" => "XXRP".to_string(),
        "USD" => "ZUSD".to_string(),
        "EUR" => "ZEUR".to_string(),
        "GBP" => "ZGBP".to_string(),
        "CAD" => "ZCAD".to_string(),
        "JPY" => "ZJPY".to_string(),
        // For assets not in the map, pass through as-is
        other => other.to_string(),
    }
}

// Helper methods
impl KrakenConnector {
    /// Convert simplified symbol to full ISO format
    ///
    /// XBTUSD → XXBTZUSD
    /// ETHUSD → XETHZUSD
    fn to_full_format(symbol: &str) -> String {
        // Common conversions
        let mut result = symbol.to_string();

        // Add X prefix to crypto if not present
        if (result.starts_with("XBT") && !result.starts_with("XXBT"))
            || ((result.starts_with("ETH") || result.starts_with("LTC"))
                && !result.starts_with("XETH") && !result.starts_with("XLTC")) {
            result = format!("X{}", result);
        }

        // Add Z prefix to fiat if not present
        if result.ends_with("USD") && !result.ends_with("ZUSD") {
            let base = &result[..result.len() - 3];
            result = format!("{}ZUSD", base);
        } else if result.ends_with("EUR") && !result.ends_with("ZEUR") {
            let base = &result[..result.len() - 3];
            result = format!("{}ZEUR", base);
        }

        result
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl FundingHistory for KrakenConnector {
    async fn get_funding_payments(
        &self,
        filter: FundingFilter,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<FundingPayment>> {
        let mut params = HashMap::new();
        params.insert("type".to_string(), "rollover".to_string());
        if let Some(start) = filter.start_time {
            // Kraken expects seconds
            params.insert("start".to_string(), (start / 1000).to_string());
        }
        if let Some(end) = filter.end_time {
            params.insert("end".to_string(), (end / 1000).to_string());
        }

        let response = self
            .post(KrakenEndpoint::SpotLedgers, params, AccountType::Spot)
            .await?;
        KrakenParser::parse_funding_payments(&response)
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl AccountLedger for KrakenConnector {
    async fn get_ledger(
        &self,
        filter: LedgerFilter,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<LedgerEntry>> {
        let mut params = HashMap::new();
        if let Some(asset) = &filter.asset {
            params.insert("asset".to_string(), asset.clone());
        }
        if let Some(start) = filter.start_time {
            params.insert("start".to_string(), (start / 1000).to_string());
        }
        if let Some(end) = filter.end_time {
            params.insert("end".to_string(), (end / 1000).to_string());
        }
        // Pagination offset
        params.insert("ofs".to_string(), "0".to_string());

        let response = self
            .post(KrakenEndpoint::SpotLedgers, params, AccountType::Spot)
            .await?;
        KrakenParser::parse_ledger(&response)
    }
}

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

/// Kraken Futures confirmed REST-historical non-OHLCV endpoints.
///
/// Hosts / paths:
/// - Mark price klines:  `GET https://futures.kraken.com/api/charts/v1/mark/{symbol}/{resolution}`
/// - Index price klines: `GET https://futures.kraken.com/api/charts/v1/spot/{symbol}/{resolution}`
/// - Funding rate hist:  `GET https://futures.kraken.com/derivatives/api/v3/historical-funding-rates?symbol=PF_XBTUSD`
///
/// NOT overridden (no dedicated endpoint):
/// - `get_premium_index_klines` — derivable from mark−spot, no native endpoint.
///   Left as default NotImplemented (trait default).
///
/// Spot: `get_recent_trades` via `GET /0/public/Trades` (Futures: WireAbsent).
/// Futures: `get_taker_volume_history` via charts/v1 analytics `aggregated-taker-volumes`
///   (analytics_type string unverified; shape assumed to match OI/LSR envelope).
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl MarketDataPublic for KrakenConnector {
    /// Mark price klines via `GET /api/charts/v1/mark/{symbol}/{resolution}`.
    ///
    /// `symbol` must be a Kraken Futures native symbol (e.g. `"PF_XBTUSD"`).
    /// Canonical `Symbol` input is normalised via `SymbolNormalizer`.
    /// `end_time` (ms) is converted to Unix seconds for the `to` query param.
    /// `limit` is not supported by the charts/v1 endpoint — the API returns all
    /// candles in the requested time window; pass `from`/`end_time` to page.
    async fn get_mark_price_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        _limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let to = end_time.map(|ms| ms / 1000);
        self.get_futures_chart_klines("mark", &symbol, interval, None, to).await
    }

    /// Index price klines via `GET /api/charts/v1/spot/{symbol}/{resolution}`.
    ///
    /// `spot` = index / reference price in Kraken Futures charts terminology.
    async fn get_index_price_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        _limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let to = end_time.map(|ms| ms / 1000);
        self.get_futures_chart_klines("spot", &symbol, interval, None, to).await
    }

    /// Premium-index klines — Kraken has no dedicated endpoint, so it's derived
    /// field-wise as `mark − spot` on matching bars (the futures basis OHLC).
    async fn get_premium_index_klines(
        &self,
        symbol: SymbolInput<'_>,
        interval: &str,
        _limit: Option<u32>,
        account_type: AccountType,
        end_time: Option<i64>,
    ) -> ExchangeResult<Vec<Kline>> {
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let to = end_time.map(|ms| ms / 1000);
        let mark = self.get_futures_chart_klines("mark", &symbol, interval, None, to).await?;
        let spot = self.get_futures_chart_klines("spot", &symbol, interval, None, to).await?;
        let spot_by_t: std::collections::HashMap<i64, &Kline> =
            spot.iter().map(|k| (k.open_time, k)).collect();
        let mut out = Vec::with_capacity(mark.len());
        for m in &mark {
            if let Some(s) = spot_by_t.get(&m.open_time) {
                out.push(Kline {
                    open_time: m.open_time,
                    open: m.open - s.open,
                    high: m.high - s.high,
                    low: m.low - s.low,
                    close: m.close - s.close,
                    volume: 0.0,
                    quote_volume: None,
                    close_time: m.close_time,
                    trades: None,
                    ..Default::default()
                });
            }
        }
        Ok(out)
    }

    /// Historical funding rates via `GET /derivatives/api/v3/historical-funding-rates`.
    ///
    /// Perpetual-only endpoint: symbol MUST use `PF_` prefix (e.g. `"PF_XBTUSD"`).
    /// Fixed-maturity futures (`PI_` prefix) return HTTP 400.
    /// `start_time` / `end_time` are not documented query params for this endpoint —
    /// only `symbol` is confirmed; the API returns all historic events for the symbol.
    async fn get_funding_rate_history(
        &self,
        symbol: SymbolInput<'_>,
        _start_time: Option<i64>,
        _end_time: Option<i64>,
        _limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<FundingRate>> {
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let mut params = HashMap::new();
        params.insert("symbol".to_string(), symbol.to_string());
        let response = self.get(
            KrakenEndpoint::FuturesHistoricalFundingRates,
            params,
            AccountType::FuturesCross,
        ).await?;
        KrakenParser::parse_historical_funding_rates(&response)
    }

    /// Open interest history via charts/v1 analytics (`open-interest`).
    /// Live-verified: `result.timestamp[]` (secs) + `result.data[]` rows of
    /// OHLC-of-OI string arrays — the close (last) is taken as the bar's OI.
    async fn get_open_interest_history(
        &self,
        symbol: SymbolInput<'_>,
        period: &str,
        start_time: Option<i64>,
        end_time: Option<i64>,
        _limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<OpenInterest>> {
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let interval_secs = (map_ohlc_interval(period) as i64) * 60;
        let from = start_time.map(|ms| ms / 1000);
        let to = end_time.map(|ms| ms / 1000);
        let resp = self.get_futures_analytics(&symbol, "open-interest", from, to, interval_secs).await?;
        KrakenParser::parse_analytics_open_interest(&resp)
    }

    /// Long/short ratio history via charts/v1 analytics (`long-short-info`).
    /// Live-verified: `result.timestamp[]` (secs) + `result.data.{longCount,
    /// shortCount}[]` — ratio = long/short.
    async fn get_long_short_ratio_history(
        &self,
        symbol: SymbolInput<'_>,
        period: &str,
        start_time: Option<i64>,
        end_time: Option<i64>,
        _limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<LongShortRatio>> {
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let interval_secs = (map_ohlc_interval(period) as i64) * 60;
        let from = start_time.map(|ms| ms / 1000);
        let to = end_time.map(|ms| ms / 1000);
        let resp = self.get_futures_analytics(&symbol, "long-short-info", from, to, interval_secs).await?;
        KrakenParser::parse_analytics_long_short(&resp, &symbol)
    }

    /// Recent public trades via `GET /0/public/Trades` (Spot only).
    ///
    /// Kraken Futures has no public recent-trades REST endpoint; futures
    /// account types return `WireAbsent`.
    ///
    /// Response is keyed by the Kraken pair-id (e.g. `XXBTZUSD`); the parser
    /// finds the array-valued key and skips the `"last"` cursor key.
    /// Trade timestamps are float seconds → integer milliseconds.
    async fn get_recent_trades(
        &self,
        symbol: SymbolInput<'_>,
        limit: Option<u32>,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<PublicTrade>> {
        match account_type {
            AccountType::FuturesCross | AccountType::FuturesIsolated => {
                return Err(ExchangeError::NotImplemented(
                    "get_recent_trades: Kraken Futures has no public recent-trades REST endpoint".into(),
                ));
            }
            _ => {}
        }
        let symbol = symbol.resolve(ExchangeId::Kraken, account_type)?;
        let mut params = HashMap::new();
        params.insert("pair".to_string(), symbol.to_string());
        if let Some(n) = limit {
            params.insert("count".to_string(), n.to_string());
        }
        let response = self.get(KrakenEndpoint::SpotTrades, params, AccountType::Spot).await?;
        KrakenParser::parse_recent_trades(&response)
    }

    // get_taker_volume_history: NOT IMPLEMENTED — wire-absent.
    // Live-probed 2026-06-14: Kraken Futures charts/v1 analytics has NO
    // taker buy/sell type — `aggregated-taker-volumes` / `taker-volume` /
    // `volume` all return {"error":"Unknown method"}. The only volume analytics
    // type is `trade-volume`, which returns a single total-volume series
    // (data:["179","1644"]) with NO buy/sell split — insufficient for TakerVolume.
    // Falls through to the default WireAbsent.
}

impl crate::core::traits::HasCapabilities for KrakenConnector {
    fn capabilities(&self) -> crate::core::types::ConnectorCapabilities {
        crate::core::types::ConnectorCapabilities {
            has_ticker: true, has_orderbook: true, has_klines: true,
            // Spot recent trades via GET /0/public/Trades; Futures: no public REST endpoint.
            has_recent_trades: true, has_exchange_info: true,
            // Confirmed REST-historical futures endpoints implemented above.
            // OI/LSR via charts/v1 analytics (open-interest / long-short-info) —
            //   analytics_type strings live-verified 2026-06-04.
            has_liquidation_history: false, has_open_interest_history: true,
            has_premium_index: false, has_long_short_ratio_history: true,
            has_funding_rate_history: true, has_mark_price_klines: true,
            has_basis_history: false,
            // No taker buy/sell analytics on Kraken (live-probed 2026-06-14: only
            // `trade-volume` total exists, no buy/sell split). Wire-absent.
            has_taker_volume_history: false,
            has_liquidation_bucket_history: false,
            has_insurance_fund: false,
            has_index_price_klines: true,
            // Derived field-wise as mark−spot klines.
            has_premium_index_klines: true,
            has_agg_trades: false,            has_market_order: true, has_limit_order: true,
            has_open_orders: true, has_order_history: true, has_user_trades: true,
            has_positions: true, has_mark_price: false, has_modify_position: false,
            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: 15, max_batch_cancel_size: 15,
            has_balance: true, has_account_info: true, has_fees: true,
            has_transfers: false, 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: false, has_ws_funding_rate: false,
            validation: self.validation_status(),
        }
    }

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

    fn trade_history_capabilities(&self) -> crate::core::types::TradeHistoryCapabilities {
        use crate::core::types::TradeHistoryTier;
        // Spot /0/public/Trades has a real `since` cursor (separate
        // endpoint from OHLC, out of Wave 2's kline-pagination scope —
        // wiring it as a deep trade-history path was not part of this
        // arc's connector list). Treated conservatively as recent-only
        // pending that separate investigation. Futures has no public
        // recent-trades REST endpoint at all (NotImplemented above).
        //
        // kline_backpage=false is a CONFIRMED VENUE CEILING, not a gap left
        // for a future fix: Wave 2 live-probed /0/public/OHLC's `since`
        // param across 1-day-ago, 30-days-ago, and epoch-zero — every call
        // returned the IDENTICAL most-recent ~720 candles. The endpoint has
        // no working backward cursor and no count/limit param either. See
        // the doc comment on `get_klines` for the full probe record.
        crate::core::types::TradeHistoryCapabilities {
            spot: TradeHistoryTier::RecentOnly { max_trades: 1000 },
            futures: TradeHistoryTier::RecentOnly { max_trades: 0 },
            kline_backpage: false,
        }
    }

    fn kline_interval_capabilities(&self) -> crate::core::types::KlineIntervalCapabilities {
        // Probe 2026-07-08: GET /0/public/OHLC?interval=0 errors
        // `{"error":["EGeneral:Invalid arguments"]}` — smallest documented
        // spot interval is 1 (minute); no seconds tier. Spot set is the
        // connector's own `map_ohlc_interval` (endpoints.rs) accepted
        // minute-int keys, translated back to canonical strings (adds
        // `15d` — Kraken's fortnightly OHLC bucket, distinct from the
        // `1w`/7-day bucket). Futures uses the separate charts/v1 API
        // (`map_futures_chart_resolution`) — no 3m/1d-multiples beyond
        // `1d`/`1w`, no seconds either (undocumented on that endpoint).
        crate::core::types::KlineIntervalCapabilities {
            spot: &["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w", "15d"],
            futures: &["1m", "5m", "15m", "30m", "1h", "4h", "12h", "1d", "1w"],
        }
    }
}
