//! Polymarket Connector
//!
//! Implements V5 core traits for Polymarket prediction markets.
//!
//! ## Supported traits
//! - `ExchangeIdentity` — returns `ExchangeId::Polymarket`
//! - `MarketData` — ping, price, orderbook, klines, ticker, exchange_info
//!
//! ## Market data mapping
//! - Symbol = `condition_id` (blockchain market identifier)
//! - Price = YES outcome probability (0.0 - 1.0)
//! - Klines = price history from CLOB `/prices-history` endpoint
//! - OrderBook = YES token order book from CLOB `/book` endpoint
//!
//! ## Public vs Authenticated
//!
//! Public mode gives access to all read endpoints.
//! Authenticated mode additionally enables /orders endpoint.

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

use reqwest::Client;

use crate::core::{
    AccountType, ExchangeError, ExchangeId, ExchangeResult, ExchangeType,
    Kline, OrderBook, Price, SymbolInfo, Ticker,
};
use crate::core::traits::{ExchangeIdentity, MarketData, MarketDataPublic};
use crate::core::types::{PublicTrade, TradeSide};
use crate::core::types::{MarketDataCapabilities, SymbolInput};
use crate::core::types::{ConnectorStats, RateLimitCapabilities, LimitModel, RestLimitPool, WsLimits, OrderbookCapabilities};
use crate::core::utils::{RuntimeLimiter, RateLimitMonitor, RateLimitPressure};

use super::auth::{PolymarketAuth, PolymarketCredentials};
use super::endpoints::{
    PolymarketEndpoints, map_interval, get_fidelity,
};
use super::parser::{
    PolymarketParser, ClobMarket, PolyMarket, PolyOrderBook, PolyMidpoint,
    PolyEvent,
    clob_market_to_symbol_info,
    poly_market_to_symbol_info,
    price_history_to_klines, poly_orderbook_to_v5,
    interval_to_ms,
};

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

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

static POLYMARKET_RATE_CAPS: RateLimitCapabilities = RateLimitCapabilities {
    model: LimitModel::Simple,
    rest_pools: POLYMARKET_RATE_POOLS,
    decaying: None,
    endpoint_weights: &[],
    ws: WsLimits {
        max_connections: None,
        max_subs_per_conn: None,
        max_msg_per_sec: None,
        max_streams_per_conn: None,
    },
};

// ═══════════════════════════════════════════════════════════════════════════
// CONNECTOR STRUCT
// ═══════════════════════════════════════════════════════════════════════════

/// Polymarket prediction markets connector
///
/// Provides access to Polymarket's REST APIs for market data, order books,
/// and price history. Supports both public (no auth) and authenticated access.
///
/// # Examples
///
/// ```ignore
/// use connectors_v5::data_feeds::prediction::polymarket::PolymarketConnector;
///
/// // Public access
/// let connector = PolymarketConnector::public();
///
/// // Get active markets
/// let markets = connector.get_active_markets(Some(20)).await?;
///
/// // Get klines for a market (condition_id as symbol)
/// let symbol = Symbol::new("0xABC123", "USDC");
/// let klines = connector.get_klines(symbol, "1h", Some(100), AccountType::Spot, None).await?;
/// ```
pub struct PolymarketConnector {
    /// Reqwest HTTP client
    client: Client,
    /// Authentication (None = public mode)
    _auth: PolymarketAuth,
    /// API base URLs
    endpoints: PolymarketEndpoints,
    /// Runtime rate limiter (Simple model: 500 req/60s)
    limiter: Arc<Mutex<RuntimeLimiter>>,
    /// Pressure monitor — logs transitions, gates non-essential requests at >= 90%
    monitor: Arc<Mutex<RateLimitMonitor>>,
    /// Cached token_id from Gamma discovery (one HTTP round-trip shared across callers)
    token_cache: Arc<OnceLock<String>>,
}

impl PolymarketConnector {
    // -----------------------------------------------------------------------
    // Constructors
    // -----------------------------------------------------------------------

    /// Create a public (no authentication) connector
    pub fn public() -> Self {
        Self {
            client: {
                #[cfg(not(target_arch = "wasm32"))]
                let b = Client::builder().timeout(Duration::from_secs(30));
                #[cfg(target_arch = "wasm32")]
                let b = Client::builder();
                b.build().unwrap_or_default()
            },
            _auth: PolymarketAuth::new(),
            endpoints: PolymarketEndpoints::default(),
            limiter: Arc::new(Mutex::new(RuntimeLimiter::from_caps(&POLYMARKET_RATE_CAPS))),
            monitor: Arc::new(Mutex::new(RateLimitMonitor::new("Polymarket"))),
            token_cache: Arc::new(OnceLock::new()),
        }
    }

    /// Create an authenticated connector with L2 credentials
    pub fn authenticated(creds: PolymarketCredentials) -> Self {
        Self {
            client: {
                #[cfg(not(target_arch = "wasm32"))]
                let b = Client::builder().timeout(Duration::from_secs(30));
                #[cfg(target_arch = "wasm32")]
                let b = Client::builder();
                b.build().unwrap_or_default()
            },
            _auth: PolymarketAuth::with_credentials(creds),
            endpoints: PolymarketEndpoints::default(),
            limiter: Arc::new(Mutex::new(RuntimeLimiter::from_caps(&POLYMARKET_RATE_CAPS))),
            monitor: Arc::new(Mutex::new(RateLimitMonitor::new("Polymarket"))),
            token_cache: Arc::new(OnceLock::new()),
        }
    }

    /// Create connector from environment variables (tries auth, falls back to public)
    pub fn from_env() -> Self {
        Self {
            client: {
                #[cfg(not(target_arch = "wasm32"))]
                let b = Client::builder().timeout(Duration::from_secs(30));
                #[cfg(target_arch = "wasm32")]
                let b = Client::builder();
                b.build().unwrap_or_default()
            },
            _auth: PolymarketAuth::from_env(),
            endpoints: PolymarketEndpoints::default(),
            limiter: Arc::new(Mutex::new(RuntimeLimiter::from_caps(&POLYMARKET_RATE_CAPS))),
            monitor: Arc::new(Mutex::new(RateLimitMonitor::new("Polymarket"))),
            token_cache: Arc::new(OnceLock::new()),
        }
    }

    // -----------------------------------------------------------------------
    // Internal HTTP helpers
    // -----------------------------------------------------------------------

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

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

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

    /// GET request to any URL, returns parsed JSON
    async fn get_url(&self, url: &str) -> ExchangeResult<serde_json::Value> {
        if !self.rate_limit_wait(1, false).await {
            return Err(ExchangeError::RateLimitExceeded {
                retry_after: None,
                message: "Rate limit budget >= 90% used; non-essential market data request dropped".to_string(),
            });
        }
        let response = self
            .client
            .get(url)
            .send()
            .await
            .map_err(|e| ExchangeError::Network(format!("Request failed: {}", e)))?;

        let status = response.status();
        if !status.is_success() {
            let body = response.text().await.unwrap_or_default();
            return Err(ExchangeError::Api {
                code: status.as_u16() as i32,
                message: format!("HTTP {}: {}", status, body.chars().take(200).collect::<String>()),
            });
        }

        let json: serde_json::Value = response
            .json()
            .await
            .map_err(|e| ExchangeError::Parse(format!("JSON parse error: {}", e)))?;

        PolymarketParser::check_error(&json)?;
        Ok(json)
    }

    /// GET request to CLOB API with optional query params
    async fn get_clob(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> ExchangeResult<serde_json::Value> {
        let base_url = format!("{}{}", self.endpoints.clob_base, path);
        let url = if params.is_empty() {
            base_url
        } else {
            let qs: String = params
                .iter()
                .map(|(k, v)| format!("{}={}", k, v))
                .collect::<Vec<_>>()
                .join("&");
            format!("{}?{}", base_url, qs)
        };
        self.get_url(&url).await
    }

    /// GET request to Gamma API with optional query params
    async fn get_gamma(
        &self,
        path: &str,
        params: &[(&str, &str)],
    ) -> ExchangeResult<serde_json::Value> {
        let base_url = format!("{}{}", self.endpoints.gamma_base, path);
        let url = if params.is_empty() {
            base_url
        } else {
            let qs: String = params
                .iter()
                .map(|(k, v)| format!("{}={}", k, v))
                .collect::<Vec<_>>()
                .join("&");
            format!("{}?{}", base_url, qs)
        };
        self.get_url(&url).await
    }

    // -----------------------------------------------------------------------
    // Polymarket-specific public methods
    // -----------------------------------------------------------------------

    /// Get paginated list of markets from CLOB API
    ///
    /// Returns `(markets, next_cursor)`. Pass `next_cursor` back to paginate.
    pub async fn get_markets(
        &self,
        limit: Option<u32>,
        next_cursor: Option<&str>,
    ) -> ExchangeResult<(Vec<ClobMarket>, Option<String>)> {
        let limit_str;
        let cursor_str;

        let mut params: Vec<(&str, &str)> = Vec::new();
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        if let Some(c) = next_cursor {
            cursor_str = c.to_string();
            params.push(("next_cursor", &cursor_str));
        }

        let response = self.get_clob("/markets", &params).await?;
        let markets = PolymarketParser::parse_clob_markets(&response)?;
        let next = PolymarketParser::get_next_cursor(&response);
        Ok((markets, next))
    }

    /// Get a specific market by condition_id (CLOB API)
    pub async fn get_market(&self, condition_id: &str) -> ExchangeResult<ClobMarket> {
        let url = format!("{}/markets/{}", self.endpoints.clob_base, condition_id);
        let response = self.get_url(&url).await?;
        PolymarketParser::parse_clob_market(&response)
    }

    /// Get order book for a specific token (CLOB API)
    pub async fn get_order_book(&self, token_id: &str) -> ExchangeResult<PolyOrderBook> {
        let response = self.get_clob("/book", &[("token_id", token_id)]).await?;
        PolymarketParser::parse_order_book(&response)
    }

    /// Get midpoint price for a token (CLOB API)
    pub async fn get_midpoint(&self, token_id: &str) -> ExchangeResult<PolyMidpoint> {
        let response = self.get_clob("/midpoint", &[("token_id", token_id)]).await?;
        PolymarketParser::parse_midpoint(&response)
    }

    /// Get last trade price for a token (CLOB API)
    pub async fn get_last_trade_price(&self, token_id: &str) -> ExchangeResult<f64> {
        let response = self.get_clob("/last-trade-price", &[("token_id", token_id)]).await?;
        PolymarketParser::parse_price(&response)
    }

    /// Get active markets only (CLOB API)
    pub async fn get_active_markets(&self, limit: Option<u32>) -> ExchangeResult<Vec<ClobMarket>> {
        let limit_str;
        let mut params: Vec<(&str, &str)> = vec![("active", "true")];
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        let response = self.get_clob("/markets", &params).await?;
        PolymarketParser::parse_clob_markets(&response)
    }

    /// Get events from Gamma API
    pub async fn get_events(&self, limit: Option<u32>) -> ExchangeResult<Vec<PolyEvent>> {
        let limit_str;
        let mut params: Vec<(&str, &str)> = Vec::new();
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        let response = self.get_gamma("/events", &params).await?;
        PolymarketParser::parse_events(&response)
    }

    /// Get a specific event by ID from Gamma API
    pub async fn get_event(&self, event_id: &str) -> ExchangeResult<PolyEvent> {
        let url = format!("{}/events/{}", self.endpoints.gamma_base, event_id);
        let response = self.get_url(&url).await?;
        PolymarketParser::parse_event(&response)
    }

    /// Get markets with enhanced metadata from Gamma API
    pub async fn get_gamma_markets(&self, limit: Option<u32>) -> ExchangeResult<Vec<PolyMarket>> {
        let limit_str;
        let mut params: Vec<(&str, &str)> = Vec::new();
        if let Some(l) = limit {
            limit_str = l.to_string();
            params.push(("limit", &limit_str));
        }
        let response = self.get_gamma("/markets", &params).await?;
        PolymarketParser::parse_gamma_markets(&response)
    }

    /// Get the primary token ID for a condition_id by fetching market details.
    ///
    /// Prefers the "Yes" outcome token. Falls back to the first available token
    /// for markets that use non-binary outcome names (e.g. team names, candidates).
    async fn get_yes_token_id(&self, condition_id: &str) -> ExchangeResult<String> {
        let market = self.get_market(condition_id).await?;
        // Prefer "Yes" outcome; fall back to first token for non-binary markets.
        let token = market
            .tokens
            .iter()
            .find(|t| t.outcome == "Yes")
            .or_else(|| market.tokens.first())
            .ok_or_else(|| {
                ExchangeError::Parse(format!(
                    "No tokens found for condition_id {}",
                    condition_id
                ))
            })?;
        Ok(token.token_id.clone())
    }

    /// Lowercase a Polymarket identifier (condition_id / token_id).
    ///
    /// CLOB API requires lowercase hex strings. Callers already pass raw market
    /// identifiers; this ensures ASCII-uppercase hex is normalised.
    fn lower_id<'a>(&self, symbol: &'a str) -> std::borrow::Cow<'a, str> {
        if symbol.chars().any(|c| c.is_ascii_uppercase()) {
            std::borrow::Cow::Owned(symbol.to_lowercase())
        } else {
            std::borrow::Cow::Borrowed(symbol)
        }
    }

    /// Fetch the most active CLOB-tradable market and return its first token_id.
    ///
    /// Uses Gamma API sorted by `volume_24hr` descending, filtered to active+open
    /// markets. The result is cached in `token_cache` so multiple callers share
    /// a single HTTP round-trip per connector lifetime.
    async fn discover_active_token_id(&self) -> ExchangeResult<String> {
        if let Some(cached) = self.token_cache.get() {
            return Ok(cached.clone());
        }

        let url = format!(
            "{}/markets?active=true&closed=false&order=volume_24hr&ascending=false&limit=1",
            self.endpoints.gamma_base
        );
        let response = self.get_url(&url).await?;

        let arr = response
            .as_array()
            .ok_or_else(|| ExchangeError::Parse("Gamma discovery: expected array".to_string()))?;

        let market = arr.first().ok_or_else(|| {
            ExchangeError::Parse("Gamma discovery: no active markets returned".to_string())
        })?;

        let token_id = market
            .get("clobTokenIds")
            .and_then(|v| {
                // clobTokenIds may be a JSON array or a stringified JSON array
                if let Some(arr) = v.as_array() {
                    arr.first().and_then(|t| t.as_str()).map(String::from)
                } else if let Some(s) = v.as_str() {
                    // parse stringified array
                    serde_json::from_str::<Vec<String>>(s)
                        .ok()
                        .and_then(|ids| ids.into_iter().next())
                } else {
                    None
                }
            })
            .ok_or_else(|| {
                ExchangeError::Parse(
                    "Gamma discovery: clobTokenIds[0] missing or empty".to_string(),
                )
            })?;

        // Best-effort cache — if another task already set it, use theirs.
        let _ = self.token_cache.set(token_id.clone());
        Ok(token_id)
    }

}

impl Default for PolymarketConnector {
    fn default() -> Self {
        Self::public()
    }
}

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

impl ExchangeIdentity for PolymarketConnector {
    fn exchange_id(&self) -> ExchangeId {
        ExchangeId::Polymarket
    }

    fn is_testnet(&self) -> bool {
        false
    }

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

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

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

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

    fn orderbook_capabilities(&self, _account_type: AccountType) -> OrderbookCapabilities {
        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: false,
            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 PolymarketConnector {
    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,
            // No recent-trades endpoint in Polymarket's public API
            has_recent_trades: false,
            // Polymarket /prices-history supports: 1m, 1h, 6h, 1d, 1w, all
            // Sub-hour intervals (3m, 5m, 15m, 30m) are mapped to "1m"; 2h/4h → "1h"; 8h/12h → "6h"
            supported_intervals: &["1m", "1h", "6h", "1d", "1w"],
            // get_fidelity() caps at 1000 via .min(1000)
            max_kline_limit: Some(1000),
            // WS supports orderbook snapshots (book) and delta updates (price_change)
            has_ws_orderbook: true,
            // WS provides last_trade_price events
            has_ws_trades: true,
            // No kline streaming channel; price history is REST-only
            has_ws_klines: false,
            // No 24h ticker stream; ticker data is REST-only
            has_ws_ticker: false,
        }
    }

    /// Check connectivity by fetching server time
    async fn ping(&self) -> ExchangeResult<()> {
        self.get_clob("/time", &[]).await?;
        Ok(())
    }

    /// Get current YES probability for a market
    ///
    /// `symbol` may be:
    /// - Empty string or a string containing "DISCOVER" → auto-discover the most
    ///   active token_id from Gamma API (same as get_ticker discovery path).
    /// - A raw token_id (large decimal number ~77 digits) → used directly.
    /// - A condition_id (0x… 66 chars) → resolved to YES token_id via CLOB `/markets`.
    async fn get_price(
        &self,
        symbol: SymbolInput<'_>,
        account_type: AccountType,
    ) -> ExchangeResult<Price> {
        let symbol = symbol.resolve(ExchangeId::Polymarket, account_type)?;
        let raw = symbol.as_ref();

        // Mirror the same token resolution logic as get_ticker so the e2e_smoke
        // "price" test cell uses a live token_id rather than a stale hardcoded one.
        let token_id: String = if raw.is_empty() || raw.contains("DISCOVER") {
            self.discover_active_token_id().await?
        } else if (raw.starts_with("0x") || raw.starts_with("0X")) && raw.len() == 66 {
            let lowered = self.lower_id(raw);
            self.get_yes_token_id(lowered.as_ref()).await?
        } else {
            raw.to_string()
        };

        // Get last trade price for the token; fall back to midpoint.
        match self.get_last_trade_price(&token_id).await {
            Ok(price) => Ok(price),
            Err(_) => {
                let midpoint = self.get_midpoint(&token_id).await?;
                Ok(midpoint.mid)
            }
        }
    }

    /// Get order book for a market's YES token
    ///
    /// `symbol` may be:
    /// - Empty string or a string containing "DISCOVER" → auto-discover active token_id.
    /// - A raw token_id (large decimal number ~77 digits) → used directly.
    /// - A condition_id (0x… 66 chars) → resolved to YES token_id via CLOB `/markets`.
    async fn get_orderbook(
        &self,
        symbol: SymbolInput<'_>,
        _depth: Option<u16>,
        account_type: AccountType,
    ) -> ExchangeResult<OrderBook> {
        let symbol = symbol.resolve(ExchangeId::Polymarket, account_type)?;
        let raw = symbol.as_ref();

        let token_id: String = if raw.is_empty() || raw.contains("DISCOVER") {
            self.discover_active_token_id().await?
        } else if (raw.starts_with("0x") || raw.starts_with("0X")) && raw.len() == 66 {
            let lowered = self.lower_id(raw);
            self.get_yes_token_id(lowered.as_ref()).await?
        } else {
            raw.to_string()
        };

        // Fetch and convert the order book
        let poly_book = self.get_order_book(&token_id).await?;
        Ok(poly_orderbook_to_v5(&poly_book))
    }

    /// Get price history as klines
    ///
    /// `symbol` may be the token_id (for efficiency) or the condition_id
    /// (the connector will look up the YES token_id).
    ///
    /// Intervals: "1m" → 1m, "1h" → 1h, "6h" → 6h, "1d" → 1d, "1w" → 1w
    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::Polymarket, account_type)?;
        let input_cow = self.lower_id(&symbol);
        let input = input_cow.as_ref();

        // Determine if this looks like a condition_id (0x... 66 chars) or a token_id.
        // A condition_id is 0x + 64 hex chars = 66 chars total.
        // A token_id is a plain numeric string (up to 78 digits).
        let token_id = if (input.starts_with("0x") || input.starts_with("0X")) && input.len() == 66
        {
            // Looks like a condition_id — look up YES token
            self.get_yes_token_id(input).await?
        } else {
            // Assume it's already a token_id
            input.to_string()
        };

        let poly_interval = map_interval(interval);
        let fidelity = get_fidelity(limit);
        let fidelity_str = fidelity.to_string();

        let response = self
            .get_clob(
                "/prices-history",
                &[
                    ("market", token_id.as_str()),
                    ("interval", poly_interval),
                    ("fidelity", fidelity_str.as_str()),
                ],
            )
            .await?;

        let history = PolymarketParser::parse_price_history(&response)?;
        let interval_ms = interval_to_ms(poly_interval);
        let klines = price_history_to_klines(history, interval_ms);

        Ok(klines)
    }

    /// Get ticker for a market.
    ///
    /// `symbol` may be:
    /// - Empty string or a string containing "DISCOVER" → auto-discover the most
    ///   active token_id from Gamma API and query CLOB midpoint + book.
    /// - A raw token_id (large decimal number ~77 digits) → used directly.
    /// - A condition_id (0x… 66 chars) → resolved to YES token_id via CLOB `/markets`.
    async fn get_ticker(
        &self,
        symbol: SymbolInput<'_>,
        account_type: AccountType,
    ) -> ExchangeResult<Ticker> {
        let symbol = symbol.resolve(ExchangeId::Polymarket, account_type)?;
        let raw = symbol.as_ref();

        // Determine the CLOB token_id to use.
        let token_id: String =
            if raw.is_empty() || raw.contains("DISCOVER") {
                // Auto-discover the highest-volume active market token.
                self.discover_active_token_id().await?
            } else if (raw.starts_with("0x") || raw.starts_with("0X")) && raw.len() == 66 {
                // condition_id → look up YES token via CLOB markets endpoint.
                let lowered = self.lower_id(raw);
                self.get_yes_token_id(lowered.as_ref()).await?
            } else {
                // Already a token_id — use as-is.
                raw.to_string()
            };

        // Fetch midpoint as last_price.
        let mid_resp = self
            .get_clob("/midpoint", &[("token_id", token_id.as_str())])
            .await?;
        let mid: f64 = mid_resp
            .get("mid")
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse().ok())
            .or_else(|| mid_resp.get("mid").and_then(|v| v.as_f64()))
            .unwrap_or(0.0);

        // Fetch best bid/ask from order book.
        let (bid, ask) = match self
            .get_clob("/book", &[("token_id", token_id.as_str())])
            .await
        {
            Ok(book_resp) => {
                let bid = book_resp
                    .get("bids")
                    .and_then(|v| v.as_array())
                    .and_then(|arr| arr.first())
                    .and_then(|entry| entry.get("price"))
                    .and_then(|p| p.as_str())
                    .and_then(|s| s.parse::<f64>().ok())
                    .unwrap_or(0.0);
                let ask = book_resp
                    .get("asks")
                    .and_then(|v| v.as_array())
                    .and_then(|arr| arr.first())
                    .and_then(|entry| entry.get("price"))
                    .and_then(|p| p.as_str())
                    .and_then(|s| s.parse::<f64>().ok())
                    .unwrap_or(0.0);
                (bid, ask)
            }
            Err(_) => (0.0, 0.0),
        };

        let now_ms = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_millis() as i64;

        Ok(Ticker {
            last_price: mid,
            bid_price: if bid > 0.0 { Some(bid) } else { None },
            ask_price: if ask > 0.0 { Some(ask) } else { None },
            volume_24h: None,
            quote_volume_24h: None,
            price_change_24h: None,
            price_change_percent_24h: None,
            high_24h: None,
            low_24h: None,
            timestamp: now_ms, ..Default::default() 
        })
    }

    /// Get all active markets as SymbolInfo
    ///
    /// Uses the Gamma API with `active=true&closed=false` filtering to return
    /// only currently-open markets. Falls back to the CLOB API if Gamma fails.
    async fn get_exchange_info(
        &self,
        account_type: AccountType,
    ) -> ExchangeResult<Vec<SymbolInfo>> {
        // Gamma API correctly filters active/open markets (CLOB `/markets` pagination
        // orders by creation date ascending and does not filter closed markets reliably).
        let gamma_result = self
            .get_gamma(
                "/markets",
                &[("active", "true"), ("closed", "false"), ("limit", "500")],
            )
            .await;

        match gamma_result {
            Ok(response) => {
                let markets = PolymarketParser::parse_gamma_markets(&response)?;
                let symbols = markets
                    .iter()
                    .map(|m| poly_market_to_symbol_info(m, account_type))
                    .collect();
                Ok(symbols)
            }
            Err(_) => {
                // Fall back to CLOB API
                let markets = self.get_active_markets(Some(500)).await?;
                let symbols = markets
                    .iter()
                    .map(|m| clob_market_to_symbol_info(m, account_type))
                    .collect();
                Ok(symbols)
            }
        }
    }
}

// Polymarket is a prediction market — no standard trading/account/positions support.
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl crate::core::traits::Trading for PolymarketConnector {
    async fn place_order(
        &self,
        _req: crate::core::types::OrderRequest,
    ) -> ExchangeResult<crate::core::types::PlaceOrderResponse> {
        Err(ExchangeError::NotImplemented(
            "Polymarket order placement not supported via standard Trading trait".into(),
        ))
    }

    async fn cancel_order(
        &self,
        _req: crate::core::types::CancelRequest,
    ) -> ExchangeResult<crate::core::types::Order> {
        Err(ExchangeError::NotImplemented(
            "Polymarket cancel order not supported via standard Trading trait".into(),
        ))
    }

    async fn get_order(
        &self,
        _symbol: &str,
        _order_id: &str,
        _account_type: AccountType,
    ) -> ExchangeResult<crate::core::types::Order> {
        Err(ExchangeError::NotImplemented(
            "Polymarket get_order not supported via standard Trading trait".into(),
        ))
    }

    async fn get_open_orders(
        &self,
        _symbol: Option<&str>,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<crate::core::types::Order>> {
        Err(ExchangeError::NotImplemented(
            "Polymarket get_open_orders not supported via standard Trading trait".into(),
        ))
    }

    async fn get_order_history(
        &self,
        _filter: crate::core::types::OrderHistoryFilter,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<crate::core::types::Order>> {
        Err(ExchangeError::NotImplemented(
            "Polymarket get_order_history not supported via standard Trading trait".into(),
        ))
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl crate::core::traits::Account for PolymarketConnector {
    async fn get_balance(
        &self,
        _query: crate::core::types::BalanceQuery,
    ) -> ExchangeResult<Vec<crate::core::types::Balance>> {
        Err(ExchangeError::NotImplemented(
            "Polymarket get_balance not supported via standard Account trait".into(),
        ))
    }

    async fn get_account_info(
        &self,
        _account_type: AccountType,
    ) -> ExchangeResult<crate::core::types::AccountInfo> {
        Err(ExchangeError::NotImplemented(
            "Polymarket get_account_info not supported".into(),
        ))
    }

    async fn get_fees(
        &self,
        _symbol: Option<&str>,
    ) -> ExchangeResult<crate::core::types::FeeInfo> {
        Err(ExchangeError::NotImplemented(
            "Polymarket get_fees not supported".into(),
        ))
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl crate::core::traits::Positions for PolymarketConnector {
    async fn get_positions(
        &self,
        _query: crate::core::types::PositionQuery,
    ) -> ExchangeResult<Vec<crate::core::types::Position>> {
        Err(ExchangeError::NotImplemented(
            "Polymarket has no positions in the traditional sense".into(),
        ))
    }

    async fn get_funding_rate(
        &self,
        _symbol: &str,
        _account_type: AccountType,
    ) -> ExchangeResult<crate::core::types::FundingRate> {
        Err(ExchangeError::NotImplemented(
            "Polymarket has no funding rate".into(),
        ))
    }

    async fn modify_position(
        &self,
        _req: crate::core::types::PositionModification,
    ) -> ExchangeResult<()> {
        Err(ExchangeError::NotImplemented(
            "Polymarket has no position modification".into(),
        ))
    }
}

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

#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl MarketDataPublic for PolymarketConnector {
    /// Recent public trades for a market.
    ///
    /// `GET https://data-api.polymarket.com/trades?market={conditionId}&limit=N`
    /// Response: `[{asset,conditionId,side,price,size,timestamp}]`
    /// side: "BUY"/"SELL". timestamp: unix seconds.
    async fn get_recent_trades(
        &self,
        symbol: SymbolInput<'_>,
        limit: Option<u32>,
        _account_type: AccountType,
    ) -> ExchangeResult<Vec<PublicTrade>> {
        let condition_id = match symbol {
            SymbolInput::Raw(s) => s.to_string(),
            SymbolInput::Canonical(c) => c.base.clone(),
        };
        let limit_str = limit.unwrap_or(100).to_string();
        let url = format!("{}/trades?market={}&limit={}",
            self.endpoints.data_base, condition_id, limit_str);
        let raw = self.get_url(&url).await?;
        let arr = raw.as_array().ok_or_else(|| {
            ExchangeError::Parse("get_recent_trades: expected array".into())
        })?;
        let mut result = Vec::with_capacity(arr.len());
        for item in arr {
            let parse_f64 = |key: &str| -> f64 {
                item.get(key)
                    .and_then(|v| v.as_str().and_then(|s| s.parse().ok()).or_else(|| v.as_f64()))
                    .unwrap_or(0.0)
            };
            let side_str = item.get("side").and_then(|v| v.as_str()).unwrap_or("BUY");
            let side = if side_str.eq_ignore_ascii_case("SELL") { TradeSide::Sell } else { TradeSide::Buy };
            let ts_secs = item.get("timestamp")
                .and_then(|v| v.as_i64().or_else(|| v.as_str().and_then(|s| s.parse().ok())))
                .unwrap_or(0);
            let id = item.get("id").and_then(|v| v.as_str())
                .or_else(|| item.get("transactionHash").and_then(|v| v.as_str()))
                .unwrap_or("")
                .to_string();
            result.push(PublicTrade {
                id,
                price: parse_f64("price"),
                quantity: parse_f64("size"),
                side,
                timestamp: ts_secs * 1000,
                ..Default::default()
            });
        }
        Ok(result)
    }
}
