//! Kalshi REST exchange client.
//!
//! Implements the Exchange trait for Kalshi's trading API.
//! Supports both API key and email/password authentication.
//! Includes fill polling and automatic token refresh on 401.

use crate::error::HorizonError;
use crate::exchanges::Exchange;
use crate::types::{Fill, OrderRequest, OrderSide, Side};
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::{Arc, Mutex, RwLock};
use tracing::{debug, error, info, warn};
use zeroize::Zeroizing;

const DEFAULT_API_URL: &str = "https://trading-api.kalshi.com/trade-api/v2";

/// Validate that a URL path parameter contains only safe characters.
/// Prevents path injection via order IDs, tickers, etc.
fn validate_url_param(param: &str, name: &str) -> Result<(), HorizonError> {
    if param.is_empty() {
        return Err(HorizonError::Exchange(format!("kalshi: {} is empty", name)));
    }
    if !param
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
    {
        return Err(HorizonError::Exchange(format!(
            "kalshi: invalid characters in {}",
            name
        )));
    }
    Ok(())
}

/// Parse a simple ISO 8601 timestamp to Unix seconds.
/// Handles formats like "2024-01-15T12:00:00Z" and "2024-01-15T12:00:00.123Z".
fn parse_iso8601_to_unix(s: &str) -> Option<f64> {
    // Expected: YYYY-MM-DDTHH:MM:SS[.fff]Z
    let s = s.trim_end_matches('Z');
    let (date_part, time_part) = s.split_once('T')?;
    let mut date_iter = date_part.split('-');
    let year: i64 = date_iter.next()?.parse().ok()?;
    let month: i64 = date_iter.next()?.parse().ok()?;
    let day: i64 = date_iter.next()?.parse().ok()?;

    let (time_whole, frac) = if let Some((w, f)) = time_part.split_once('.') {
        let frac_secs: f64 = format!("0.{}", f).parse().unwrap_or(0.0);
        (w, frac_secs)
    } else {
        (time_part, 0.0)
    };

    let mut time_iter = time_whole.split(':');
    let hour: i64 = time_iter.next()?.parse().ok()?;
    let min: i64 = time_iter.next()?.parse().ok()?;
    let sec: i64 = time_iter.next()?.parse().ok()?;

    // Validate date/time ranges to prevent panics
    if month < 1 || month > 12 || day < 1 || day > 31 {
        return None;
    }
    if hour < 0 || hour > 23 || min < 0 || min > 59 || sec < 0 || sec > 59 {
        return None;
    }

    // Per-month day validation
    let max_days = match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 => if is_leap(year) { 29 } else { 28 },
        _ => return None,
    };
    if day > max_days {
        return None;
    }

    // Days from year 1970 to the given date (simplified, no leap second handling)
    let mut days: i64 = 0;
    for y in 1970..year {
        days += if is_leap(y) { 366 } else { 365 };
    }
    let month_days = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
    for m in 1..month {
        days += month_days[m as usize];
        if m == 2 && is_leap(year) {
            days += 1;
        }
    }
    days += day - 1;

    let total_secs = days * 86400 + hour * 3600 + min * 60 + sec;
    Some(total_secs as f64 + frac)
}

fn is_leap(y: i64) -> bool {
    (y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}

pub struct KalshiExchange {
    api_url: String,
    auth_token: RwLock<Option<Zeroizing<String>>>,
    email: Option<Zeroizing<String>>,
    password: Option<Zeroizing<String>>,
    api_key: Option<Zeroizing<String>>,
    client: reqwest::Client,
    runtime: Arc<tokio::runtime::Runtime>,
    fills: Mutex<Vec<Fill>>,
    next_fill_id: Mutex<u64>,
    /// Cursor for incremental fill polling (last seen fill ID or timestamp).
    last_fill_cursor: Mutex<Option<String>>,
    /// Tracks open order IDs per market for per-market cancellation.
    open_orders: Mutex<HashMap<String, Vec<String>>>,
    /// Seen fill IDs for deduplication (prevents duplicate fills on cursor overlap).
    seen_fill_ids: Mutex<HashSet<String>>,
    /// FIFO order for seen fill IDs (oldest first for eviction).
    seen_fill_order: Mutex<VecDeque<String>>,
}

impl KalshiExchange {
    pub fn new(
        email: Option<String>,
        password: Option<String>,
        api_key: Option<String>,
        api_url: Option<String>,
        runtime: Arc<tokio::runtime::Runtime>,
    ) -> Self {
        Self {
            api_url: api_url.unwrap_or_else(|| DEFAULT_API_URL.to_string()),
            auth_token: RwLock::new(None),
            email: email.map(Zeroizing::new),
            password: password.map(Zeroizing::new),
            api_key: api_key.map(Zeroizing::new),
            client: reqwest::Client::builder()
                .timeout(std::time::Duration::from_secs(10))
                .connect_timeout(std::time::Duration::from_secs(5))
                .build()
                .unwrap_or_else(|_| reqwest::Client::new()),
            runtime,
            fills: Mutex::new(Vec::new()),
            next_fill_id: Mutex::new(1),
            last_fill_cursor: Mutex::new(None),
            open_orders: Mutex::new(HashMap::new()),
            seen_fill_ids: Mutex::new(HashSet::new()),
            seen_fill_order: Mutex::new(VecDeque::new()),
        }
    }

    /// Login to Kalshi and obtain an auth token.
    async fn login(&self) -> Result<String, HorizonError> {
        // If we have an API key, use it directly
        if let Some(ref key) = self.api_key {
            return Ok((**key).clone());
        }

        let email = self
            .email
            .as_ref()
            .ok_or_else(|| HorizonError::Exchange("kalshi: email required for login".into()))?;
        let password = self
            .password
            .as_ref()
            .ok_or_else(|| HorizonError::Exchange("kalshi: password required for login".into()))?;

        let url = format!("{}/login", self.api_url);
        let body = serde_json::json!({
            "email": &**email,
            "password": &**password,
        });

        info!("kalshi: logging in");

        let resp = self
            .client
            .post(&url)
            .json(&body)
            .send()
            .await
            .map_err(|e| HorizonError::Exchange(format!("kalshi login failed: {}", e)))?;

        if !resp.status().is_success() {
            let status = resp.status();
            let body = resp.text().await.unwrap_or_default();
            debug!(status = %status, body = %body, "kalshi: login response body");
            error!(status = %status, "kalshi: login failed");
            return Err(HorizonError::Exchange(format!(
                "kalshi login failed ({})",
                status
            )));
        }

        let json: serde_json::Value = resp
            .json()
            .await
            .map_err(|e| HorizonError::Exchange(format!("kalshi login parse error: {}", e)))?;

        let token = json
            .get("token")
            .and_then(|v| v.as_str())
            .ok_or_else(|| HorizonError::Exchange("kalshi: no token in login response".into()))?
            .to_string();

        // Cache the token (wrapped in Zeroizing for secure erasure on drop)
        if let Ok(mut guard) = self.auth_token.write() {
            *guard = Some(Zeroizing::new(token.clone()));
        }

        info!("kalshi: login successful");
        Ok(token)
    }

    /// Get the current auth token, logging in if necessary.
    async fn get_token(&self) -> Result<String, HorizonError> {
        {
            if let Ok(token) = self.auth_token.read() {
                if let Some(ref t) = *token {
                    return Ok((**t).clone());
                }
            }
        }
        self.login().await
    }

    /// Clear the cached auth token (on 401).
    fn clear_token(&self) {
        if let Ok(mut guard) = self.auth_token.write() {
            *guard = None;
        }
    }

    /// Convert Side + OrderSide to Kalshi action and side strings.
    ///
    /// Kalshi uses:
    /// - "action": "buy" or "sell"
    /// - "side": "yes" or "no"
    ///
    /// Mapping:
    /// - Buy Yes  → action=buy,  side=yes
    /// - Sell Yes → action=sell, side=yes
    /// - Buy No   → action=buy,  side=no
    /// - Sell No  → action=sell, side=no
    fn kalshi_action(order_side: OrderSide) -> &'static str {
        match order_side {
            OrderSide::Buy => "buy",
            OrderSide::Sell => "sell",
        }
    }

    fn kalshi_side(side: Side) -> &'static str {
        match side {
            Side::Yes => "yes",
            Side::No => "no",
        }
    }

    /// Map OrderType to Kalshi type string.
    fn kalshi_order_type(order_type: crate::types::OrderType) -> &'static str {
        match order_type {
            crate::types::OrderType::Market => "market",
            crate::types::OrderType::Limit => "limit",
        }
    }

    /// Map TimeInForce to Kalshi time_in_force string.
    /// For market orders, always returns "fill_or_kill".
    fn kalshi_tif(order_type: crate::types::OrderType, tif: crate::types::TimeInForce) -> &'static str {
        if order_type == crate::types::OrderType::Market {
            return "fill_or_kill";
        }
        match tif {
            crate::types::TimeInForce::GTC => "good_till_canceled",
            crate::types::TimeInForce::FOK => "fill_or_kill",
            crate::types::TimeInForce::FAK => "immediate_or_cancel",
            crate::types::TimeInForce::GTD => {
                tracing::warn!("kalshi: GTD time-in-force not supported, downgrading to GTC");
                "good_till_canceled"
            }
        }
    }

    /// Submit order to Kalshi, with automatic retry on 401 and 429.
    async fn submit_order_async(&self, req: &OrderRequest) -> Result<String, HorizonError> {
        let url = format!("{}/portfolio/orders", self.api_url);
        // Validate price bounds before conversion to avoid undefined f64→i64 casts
        if req.order_type != crate::types::OrderType::Market
            && (req.price < 0.01 || req.price > 0.99 || req.price.is_nan())
        {
            return Err(HorizonError::Exchange(format!(
                "invalid price for Kalshi: {}", req.price
            )));
        }
        if req.size <= 0.0 || req.size.is_nan() {
            return Err(HorizonError::Exchange(format!(
                "invalid size for Kalshi: {}", req.size
            )));
        }
        let price_cents = (req.price * 100.0).round() as i64;
        let count = req.size.round() as i64;
        if count < 1 {
            return Err(HorizonError::Exchange(format!(
                "kalshi: order size {} rounds to {} contracts (minimum 1)",
                req.size, count
            )));
        }
        let order_type_str = Self::kalshi_order_type(req.order_type);
        let tif_str = Self::kalshi_tif(req.order_type, req.time_in_force);
        let is_market = req.order_type == crate::types::OrderType::Market;

        let body = match req.side {
            Side::Yes => {
                let mut obj = serde_json::json!({
                    "ticker": req.market_id,
                    "action": Self::kalshi_action(req.order_side),
                    "side": "yes",
                    "type": order_type_str,
                    "count": count,
                    "time_in_force": tif_str,
                });
                if !is_market {
                    obj["yes_price"] = serde_json::json!(price_cents);
                }
                obj
            }
            Side::No => {
                let mut obj = serde_json::json!({
                    "ticker": req.market_id,
                    "action": Self::kalshi_action(req.order_side),
                    "side": "no",
                    "type": order_type_str,
                    "count": count,
                    "time_in_force": tif_str,
                });
                if !is_market {
                    obj["no_price"] = serde_json::json!(price_cents);
                }
                obj
            }
        };

        debug!(
            market = %req.market_id,
            action = Self::kalshi_action(req.order_side),
            side = Self::kalshi_side(req.side),
            price_cents = price_cents,
            count = count,
            "kalshi: submitting order"
        );

        let mut backoff_ms: u64 = 1000;
        let mut auth_retried = false;

        for attempt in 0..5u32 {
            let token = self.get_token().await?;

            let resp = self
                .client
                .post(&url)
                .bearer_auth(&token)
                .json(&body)
                .send()
                .await
                .map_err(|e| HorizonError::Exchange(format!("kalshi order failed: {}", e)))?;

            let status_code = resp.status().as_u16();

            if status_code == 401 && !auth_retried {
                warn!("kalshi: auth expired, re-authenticating");
                self.clear_token();
                auth_retried = true;
                continue;
            }

            if status_code == 429 && attempt < 4 {
                warn!(attempt = attempt + 1, backoff_ms = backoff_ms, "kalshi: rate limited (429), retrying");
                tokio::time::sleep(tokio::time::Duration::from_millis(backoff_ms)).await;
                backoff_ms = (backoff_ms * 2).min(30_000);
                continue;
            }

            if !resp.status().is_success() {
                let status = resp.status();
                let body_text = resp.text().await.unwrap_or_default();
                error!(status = %status, body = %body_text, "kalshi: order rejected");
                return Err(HorizonError::Exchange(format!(
                    "kalshi order rejected ({}): {}",
                    status, body_text
                )));
            }

            let json: serde_json::Value = resp
                .json()
                .await
                .map_err(|e| HorizonError::Exchange(format!("kalshi parse error: {}", e)))?;

            let order_id = json
                .get("order")
                .and_then(|o| o.get("order_id"))
                .and_then(|v| v.as_str())
                .filter(|s| !s.is_empty())
                .map(|s| s.to_string())
                .ok_or_else(|| {
                    HorizonError::Exchange(format!(
                        "kalshi: response missing order_id: {}",
                        json
                    ))
                })?;

            info!(order_id = %order_id, "kalshi: order accepted");
            return Ok(order_id);
        }

        Err(HorizonError::Exchange("kalshi: retry attempts exhausted".into()))
    }

    /// Cancel order on Kalshi, with automatic retry on 401.
    async fn cancel_order_async(&self, order_id: &str) -> Result<(), HorizonError> {
        validate_url_param(order_id, "order_id")?;
        let url = format!("{}/portfolio/orders/{}", self.api_url, order_id);

        for attempt in 0..2 {
            let token = self.get_token().await?;

            let resp = self
                .client
                .delete(&url)
                .bearer_auth(&token)
                .send()
                .await
                .map_err(|e| HorizonError::Exchange(format!("kalshi cancel failed: {}", e)))?;

            if resp.status().as_u16() == 401 && attempt == 0 {
                warn!("kalshi: auth expired on cancel, re-authenticating");
                self.clear_token();
                continue;
            }

            if !resp.status().is_success() {
                let status = resp.status();
                let body = resp.text().await.unwrap_or_default();
                return Err(HorizonError::Exchange(format!(
                    "kalshi cancel failed ({}): {}",
                    status, body
                )));
            }

            debug!(order_id = %order_id, "kalshi: order canceled");
            return Ok(());
        }

        Err(HorizonError::Exchange("kalshi: cancel auth retry exhausted".into()))
    }

    /// Cancel all orders on Kalshi, with automatic retry on 401.
    async fn cancel_all_async(&self) -> Result<usize, HorizonError> {
        let url = format!("{}/portfolio/orders", self.api_url);

        for attempt in 0..2 {
            let token = self.get_token().await?;

            let resp = self
                .client
                .delete(&url)
                .bearer_auth(&token)
                .send()
                .await
                .map_err(|e| HorizonError::Exchange(format!("kalshi cancel-all failed: {}", e)))?;

            if resp.status().as_u16() == 401 && attempt == 0 {
                warn!("kalshi: auth expired on cancel-all, re-authenticating");
                self.clear_token();
                continue;
            }

            if !resp.status().is_success() {
                let status = resp.status();
                let body = resp.text().await.unwrap_or_default();
                return Err(HorizonError::Exchange(format!(
                    "kalshi cancel-all failed ({}): {}",
                    status, body
                )));
            }

            let json: serde_json::Value = resp.json().await.unwrap_or_else(|e| {
                warn!(error = %e, "kalshi: cancel-all response parse failed");
                serde_json::Value::Null
            });
            let count = json
                .get("reduced_count")
                .and_then(|v| v.as_u64())
                .unwrap_or(0);

            info!(count = count, "kalshi: canceled all orders");
            return Ok(count as usize);
        }

        Err(HorizonError::Exchange("kalshi: cancel-all auth retry exhausted".into()))
    }

    /// Poll for recent fills from Kalshi.
    async fn poll_fills_async(&self) -> Result<Vec<Fill>, HorizonError> {
        let token = match self.get_token().await {
            Ok(t) => t,
            Err(e) => {
                debug!(error = %e, "kalshi: skipping fill poll (no auth)");
                return Ok(Vec::new());
            }
        };

        let mut url = format!("{}/portfolio/fills", self.api_url);

        // Add cursor for incremental polling (URL-encoded to prevent injection)
        if let Ok(cursor) = self.last_fill_cursor.lock() {
            if let Some(ref c) = *cursor {
                let encoded: String =
                    url::form_urlencoded::Serializer::new(String::new())
                        .append_pair("cursor", c)
                        .finish();
                url = format!("{}?{}", url, encoded);
            }
        }

        let resp = self
            .client
            .get(&url)
            .bearer_auth(&token)
            .send()
            .await
            .map_err(|e| {
                HorizonError::Exchange(format!("kalshi: fill poll failed: {}", e))
            })?;

        if !resp.status().is_success() {
            let status = resp.status();
            if status.as_u16() == 401 {
                self.clear_token();
            }
            debug!(status = %status, "kalshi: fill poll returned non-200");
            return Ok(Vec::new());
        }

        let json: serde_json::Value = resp.json().await.map_err(|e| {
            HorizonError::Exchange(format!("kalshi: fill parse error: {}", e))
        })?;

        // Update cursor for next poll
        if let Some(cursor) = json.get("cursor").and_then(|v| v.as_str()) {
            if !cursor.is_empty() {
                if let Ok(mut guard) = self.last_fill_cursor.lock() {
                    *guard = Some(cursor.to_string());
                }
            }
        }

        let mut new_fills = Vec::new();

        // Kalshi /portfolio/fills response: {"fills": [...], "cursor": "..."}
        let fills_arr = json.get("fills").and_then(|v| v.as_array());

        if let Some(fills_arr) = fills_arr {
            let mut next_id = self.next_fill_id.lock().unwrap_or_else(|e| e.into_inner());

            for fill_json in fills_arr {
                let fill_id_str = fill_json
                    .get("fill_id")
                    .or_else(|| fill_json.get("id"))
                    .and_then(|v| v.as_str())
                    .unwrap_or("");

                let order_id = fill_json
                    .get("order_id")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let ticker = fill_json
                    .get("ticker")
                    .and_then(|v| v.as_str())
                    .unwrap_or("")
                    .to_string();

                let count = fill_json
                    .get("count")
                    .and_then(|v| v.as_f64().or_else(|| v.as_i64().map(|i| i as f64)))
                    .unwrap_or(0.0);

                let action = fill_json.get("action").and_then(|v| v.as_str()).unwrap_or("buy");
                let side_str = fill_json.get("side").and_then(|v| v.as_str()).unwrap_or("yes");

                let order_side = if action.eq_ignore_ascii_case("buy") {
                    OrderSide::Buy
                } else {
                    OrderSide::Sell
                };

                let side = if side_str.eq_ignore_ascii_case("yes") {
                    Side::Yes
                } else {
                    Side::No
                };

                // Kalshi returns price in cents — use side-appropriate price field
                let price_cents = if side == Side::No {
                    fill_json.get("no_price")
                        .or_else(|| fill_json.get("price"))
                        .and_then(|v| v.as_f64().or_else(|| v.as_i64().map(|i| i as f64)))
                        .unwrap_or(0.0)
                } else {
                    fill_json.get("yes_price")
                        .or_else(|| fill_json.get("price"))
                        .and_then(|v| v.as_f64().or_else(|| v.as_i64().map(|i| i as f64)))
                        .unwrap_or(0.0)
                };
                let price = price_cents / 100.0;

                let ts = fill_json
                    .get("created_time")
                    .or_else(|| fill_json.get("ts"))
                    .and_then(|v| {
                        v.as_f64().or_else(|| v.as_str().and_then(|s| {
                            s.parse::<f64>().ok().or_else(|| parse_iso8601_to_unix(s))
                        }))
                    })
                    .unwrap_or(0.0);

                let fill_id = if fill_id_str.is_empty() {
                    let id = format!("kalshi_fill_{}", *next_id);
                    *next_id += 1;
                    id
                } else {
                    fill_id_str.to_string()
                };

                // Dedup by fill ID (prevents duplicate fills on cursor overlap/restart)
                {
                    let mut seen = self.seen_fill_ids.lock().unwrap_or_else(|e| e.into_inner());
                    if seen.contains(&fill_id) {
                        continue;
                    }
                    seen.insert(fill_id.clone());
                    // FIFO eviction: track insertion order, evict oldest when over cap
                    if let Ok(mut order) = self.seen_fill_order.lock() {
                        order.push_back(fill_id.clone());
                        while seen.len() > 10_000 {
                            if let Some(oldest) = order.pop_front() {
                                seen.remove(&oldest);
                            } else {
                                break;
                            }
                        }
                    }
                }

                new_fills.push(Fill {
                    fill_id,
                    order_id,
                    market_id: ticker,
                    side,
                    order_side,
                    price,
                    size: count,
                    fee: fill_json
                        .get("fee")
                        .or_else(|| fill_json.get("taker_fee"))
                        .and_then(|v| v.as_f64().or_else(|| v.as_i64().map(|i| i as f64)))
                        .map(|f| f / 100.0) // Kalshi returns fees in cents
                        .unwrap_or(0.0),
                    timestamp: ts,
                    token_id: None,
                    exchange: "kalshi".to_string(),
                    is_maker: false,
                });
            }
        }

        if !new_fills.is_empty() {
            info!(count = new_fills.len(), "kalshi: polled new fills");
        }

        Ok(new_fills)
    }

    /// Fetch positions from Kalshi for reconciliation.
    async fn fetch_positions_async(&self) -> Result<Vec<crate::types::Position>, HorizonError> {
        let token = match self.get_token().await {
            Ok(t) => t,
            Err(e) => {
                debug!(error = %e, "kalshi: skipping position fetch (no auth)");
                return Ok(Vec::new());
            }
        };

        let url = format!("{}/portfolio/positions", self.api_url);
        let resp = self
            .client
            .get(&url)
            .bearer_auth(&token)
            .send()
            .await
            .map_err(|e| HorizonError::Exchange(format!("kalshi: position fetch failed: {}", e)))?;

        if !resp.status().is_success() {
            let status = resp.status();
            if status.as_u16() == 401 {
                self.clear_token();
            }
            debug!(status = %status, "kalshi: position fetch returned non-200");
            return Ok(Vec::new());
        }

        let json: serde_json::Value = resp.json().await.map_err(|e| {
            HorizonError::Exchange(format!("kalshi: position parse error: {}", e))
        })?;

        let mut positions = Vec::new();
        let arr = json
            .get("market_positions")
            .or_else(|| json.get("positions"))
            .and_then(|v| v.as_array());

        if let Some(arr) = arr {
            for p in arr {
                let ticker = p.get("ticker").and_then(|v| v.as_str()).unwrap_or("");
                let position = p
                    .get("position")
                    .and_then(|v| v.as_f64().or_else(|| v.as_i64().map(|i| i as f64)))
                    .unwrap_or(0.0);
                let avg_price_cents = p
                    .get("average_price")
                    .and_then(|v| v.as_f64().or_else(|| v.as_i64().map(|i| i as f64)))
                    .unwrap_or(0.0);
                let side_str = p.get("side").and_then(|v| v.as_str()).unwrap_or("yes");

                if position.abs() > 0.0 {
                    positions.push(crate::types::Position {
                        market_id: ticker.to_string(),
                        side: if side_str.eq_ignore_ascii_case("yes") {
                            crate::types::Side::Yes
                        } else {
                            crate::types::Side::No
                        },
                        size: position,
                        avg_entry_price: avg_price_cents / 100.0,
                        realized_pnl: 0.0,
                        unrealized_pnl: 0.0,
                        token_id: None,
                        exchange: "kalshi".to_string(),
                    });
                }
            }
        }

        if !positions.is_empty() {
            info!(count = positions.len(), "kalshi: fetched positions for reconciliation");
        }

        Ok(positions)
    }
}

impl Exchange for KalshiExchange {
    fn submit_order(&self, req: &OrderRequest) -> Result<String, HorizonError> {
        let req_clone = req.clone();
        let order_id = self.runtime.block_on(self.submit_order_async(&req_clone))?;

        // Track order_id per market for per-market cancellation
        if let Ok(mut map) = self.open_orders.lock() {
            map.entry(req.market_id.clone())
                .or_default()
                .push(order_id.clone());
            // Evict empty market entries to bound growth
            map.retain(|_, ids| !ids.is_empty());
            // Hard cap: if tracking > 10k order IDs total, clear stale entries
            let total: usize = map.values().map(|v| v.len()).sum();
            if total > 10_000 {
                warn!("kalshi: open_orders tracking exceeds 10k, clearing stale entries");
                map.clear();
                map.entry(req.market_id.clone())
                    .or_default()
                    .push(order_id.clone());
            }
        }

        Ok(order_id)
    }

    fn cancel_order(&self, order_id: &str) -> Result<(), HorizonError> {
        let id = order_id.to_string();
        self.runtime.block_on(self.cancel_order_async(&id))?;

        // Remove from open_orders tracking
        if let Ok(mut map) = self.open_orders.lock() {
            for ids in map.values_mut() {
                ids.retain(|oid| oid != order_id);
            }
        }

        Ok(())
    }

    fn cancel_all(&self) -> Result<usize, HorizonError> {
        let count = self.runtime.block_on(self.cancel_all_async())?;

        // Clear all tracked orders
        if let Ok(mut map) = self.open_orders.lock() {
            map.clear();
        }

        Ok(count)
    }

    fn cancel_for_market(&self, market_id: &str) -> Result<usize, HorizonError> {
        let order_ids: Vec<String> = {
            let map = self.open_orders.lock().unwrap_or_else(|e| e.into_inner());
            map.get(market_id).cloned().unwrap_or_default()
        };

        if order_ids.is_empty() {
            return Ok(0);
        }

        let mut successfully_canceled = Vec::new();
        for oid in &order_ids {
            match self.runtime.block_on(self.cancel_order_async(oid)) {
                Ok(()) => successfully_canceled.push(oid.clone()),
                Err(e) => {
                    debug!(order_id = %oid, error = %e, "kalshi: cancel_for_market: order cancel failed (may already be filled)");
                }
            }
        }

        // Only remove successfully canceled IDs from tracking
        if let Ok(mut map) = self.open_orders.lock() {
            if let Some(ids) = map.get_mut(market_id) {
                ids.retain(|oid| !successfully_canceled.contains(oid));
                if ids.is_empty() {
                    map.remove(market_id);
                }
            }
        }

        let canceled = successfully_canceled.len();
        info!(market_id = %market_id, canceled = canceled, total = order_ids.len(), "kalshi: canceled orders for market");
        Ok(canceled)
    }

    fn drain_fills(&self) -> Vec<Fill> {
        // Poll for new fills from the API
        match self.runtime.block_on(self.poll_fills_async()) {
            Ok(new_fills) => {
                if !new_fills.is_empty() {
                    // Remove filled order IDs from open_orders tracking
                    if let Ok(mut map) = self.open_orders.lock() {
                        for fill in &new_fills {
                            for ids in map.values_mut() {
                                ids.retain(|oid| oid != &fill.order_id);
                            }
                        }
                        map.retain(|_, ids| !ids.is_empty());
                    }

                    if let Ok(mut fills) = self.fills.lock() {
                        fills.extend(new_fills);
                    }
                }
            }
            Err(e) => {
                warn!(error = %e, "kalshi: fill polling error");
            }
        }

        // Return accumulated fills
        match self.fills.lock() {
            Ok(mut fills) => std::mem::take(&mut *fills),
            Err(_) => {
                error!("kalshi: fills lock poisoned");
                Vec::new()
            }
        }
    }

    fn name(&self) -> &str {
        "kalshi"
    }

    fn fetch_positions(&self) -> Result<Vec<crate::types::Position>, HorizonError> {
        self.runtime.block_on(self.fetch_positions_async())
    }
}

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

    #[test]
    fn test_parse_iso8601_basic() {
        // 2024-01-01T00:00:00Z should be 1704067200
        let ts = parse_iso8601_to_unix("2024-01-01T00:00:00Z").unwrap();
        assert!((ts - 1704067200.0).abs() < 1.0);
    }

    #[test]
    fn test_parse_iso8601_with_fractional() {
        let ts = parse_iso8601_to_unix("2024-01-15T12:30:45.123Z").unwrap();
        // Should be Jan 15 2024 12:30:45.123 UTC
        assert!(ts > 1705300000.0);
        // Verify fractional part
        let frac = ts - ts.floor();
        assert!((frac - 0.123).abs() < 0.01);
    }

    #[test]
    fn test_parse_iso8601_invalid() {
        assert!(parse_iso8601_to_unix("not-a-date").is_none());
        assert!(parse_iso8601_to_unix("").is_none());
    }

    #[test]
    fn test_parse_iso8601_out_of_range() {
        // Month > 12
        assert!(parse_iso8601_to_unix("2024-13-01T00:00:00Z").is_none());
        // Month = 0
        assert!(parse_iso8601_to_unix("2024-00-01T00:00:00Z").is_none());
        // Day = 0
        assert!(parse_iso8601_to_unix("2024-01-00T00:00:00Z").is_none());
        // Day > 31
        assert!(parse_iso8601_to_unix("2024-01-32T00:00:00Z").is_none());
        // Hour > 23
        assert!(parse_iso8601_to_unix("2024-01-01T25:00:00Z").is_none());
    }

    #[test]
    fn test_is_leap() {
        assert!(is_leap(2024));
        assert!(!is_leap(2023));
        assert!(is_leap(2000));
        assert!(!is_leap(1900));
    }

    #[test]
    fn test_parse_iso8601_feb31() {
        assert!(parse_iso8601_to_unix("2024-02-31T00:00:00Z").is_none());
        assert!(parse_iso8601_to_unix("2024-02-30T00:00:00Z").is_none());
        assert!(parse_iso8601_to_unix("2024-02-29T00:00:00Z").is_some()); // leap year
        assert!(parse_iso8601_to_unix("2023-02-29T00:00:00Z").is_none()); // non-leap
    }
}
