//! Kalshi orderbook feed.
//!
//! Polls Kalshi's REST API for orderbook data and updates both a shared
//! FeedSnapshot (best bid/ask) and OrderbookSnapshot (full L2 depth).

use crate::feeds::{FeedSnapshot, MetricsStore, OrderbookStore};
use crate::orderbook::OrderbookSnapshot;
use dashmap::DashMap;
use std::sync::Arc;
use tracing::{debug, warn};

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

/// Run the Kalshi orderbook feed (polling) until shutdown is signaled.
pub async fn run_kalshi_book(
    name: String,
    ticker: String,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    orderbooks: OrderbookStore,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    // Validate ticker before interpolating into URL to prevent path injection
    if !ticker
        .bytes()
        .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_')
    {
        warn!(ticker = %ticker, "kalshi feed: invalid ticker characters, aborting");
        return;
    }

    let 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());
    let url = format!("{}/markets/{}/orderbook", KALSHI_API_URL, ticker);
    let base_interval_ms: u64 = 2000;
    let mut current_interval_ms: u64 = base_interval_ms;

    // Pre-compute source string to avoid allocation per poll
    let source = format!("kalshi:{}", ticker);

    debug!(ticker = %ticker, url = %url, "kalshi feed: starting orderbook poll");

    // Mark connected
    if let Some(mut m) = metrics.get_mut(&name) {
        m.is_connected = true;
    }

    loop {
        tokio::select! {
            _ = global_shutdown.notified() => break,
            _ = feed_shutdown.notified() => break,
            _ = tokio::time::sleep(tokio::time::Duration::from_millis(current_interval_ms)) => {
                match client.get(&url).send().await {
                    Ok(resp) => {
                        if resp.status().as_u16() == 429 {
                            // Rate limited: backoff
                            current_interval_ms = (current_interval_ms * 2).min(30_000);
                            warn!(
                                ticker = %ticker,
                                next_interval_ms = current_interval_ms,
                                "kalshi feed: rate limited (429), backing off"
                            );
                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.error_count += 1;
                                m.last_error = "rate limited (429)".to_string();
                            }
                            continue;
                        }

                        if !resp.status().is_success() {
                            let status = resp.status();
                            warn!(
                                ticker = %ticker,
                                status = %status,
                                "kalshi feed: orderbook request failed"
                            );
                            current_interval_ms = (current_interval_ms * 2).min(30_000);
                            if let Some(mut m) = metrics.get_mut(&name) {
                                m.error_count += 1;
                                m.last_error = format!("HTTP {}", status);
                            }
                            continue;
                        }

                        // Success: reset interval
                        current_interval_ms = base_interval_ms;

                        if let Ok(body) = resp.text().await {
                            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&body) {
                                let orderbook = json.get("orderbook").unwrap_or(&json);

                                // Extract all levels for L2 depth
                                let yes_levels = extract_all(orderbook, "yes");
                                let no_levels = extract_all(orderbook, "no");

                                let best_bid = yes_levels.first().map(|&(p, _)| p).unwrap_or(0.0);
                                let best_no = no_levels.first().map(|&(p, _)| p).unwrap_or(0.0);
                                let best_ask = if best_no > 0.0 && best_no <= 1.0 {
                                    1.0 - best_no
                                } else {
                                    0.0
                                };

                                let mid = if best_bid > 0.0 && best_ask > 0.0 && best_ask < 1.0 {
                                    (best_bid + best_ask) / 2.0
                                } else if best_bid > 0.0 {
                                    best_bid
                                } else if best_ask > 0.0 {
                                    best_ask
                                } else {
                                    0.0
                                };

                                if mid > 0.0 {
                                    let now = std::time::SystemTime::now()
                                        .duration_since(std::time::UNIX_EPOCH)
                                        .unwrap_or_default()
                                        .as_secs_f64();

                                    let snap = FeedSnapshot {
                                        price: mid,
                                        timestamp: now,
                                        source: source.clone(),
                                        bid: best_bid,
                                        ask: best_ask,
                                        volume_24h: 0.0,
                                        last_trade_size: 0.0,
                                        last_trade_is_buy: false,
                                    };

                                    snapshots.insert(name.clone(), snap);

                                    // Build L2 orderbook: convert YES/NO to bid/ask
                                    // Bids = YES side (sorted desc by price)
                                    // Asks = complement of NO side (sorted asc)
                                    let bid_levels = yes_levels; // already sorted desc
                                    let mut ask_levels: Vec<(f64, f64)> = no_levels
                                        .into_iter()
                                        .map(|(p, s)| (1.0 - p, s))
                                        .collect();
                                    ask_levels.sort_by(|a, b| {
                                        a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal)
                                    });

                                    let book = OrderbookSnapshot::from_levels(
                                        bid_levels,
                                        ask_levels,
                                        now,
                                        source.clone(),
                                    );
                                    orderbooks.insert(name.clone(), book);

                                    if let Some(mut m) = metrics.get_mut(&name) {
                                        m.update_count += 1;
                                        m.last_update_time = now;
                                    }
                                }
                            }
                        }
                    }
                    Err(e) => {
                        warn!(
                            ticker = %ticker,
                            error = %e,
                            "kalshi feed: poll failed"
                        );
                        current_interval_ms = (current_interval_ms * 2).min(30_000);
                        if let Some(mut m) = metrics.get_mut(&name) {
                            m.error_count += 1;
                            m.last_error = e.to_string();
                        }
                    }
                }
            }
        }
    }

    // Mark disconnected
    if let Some(mut m) = metrics.get_mut(&name) {
        m.is_connected = false;
    }
}

/// Extract all levels from a Kalshi orderbook side.
/// Kalshi format: {"yes": [[price_cents, quantity], ...], "no": [...]}
/// Returns Vec<(price, size)>, sorted descending by price (highest first).
fn extract_all(orderbook: &serde_json::Value, side: &str) -> Vec<(f64, f64)> {
    let arr = match orderbook.get(side).and_then(|v| v.as_array()) {
        Some(a) => a,
        None => return Vec::new(),
    };

    let mut levels: Vec<(f64, f64)> = arr
        .iter()
        .filter_map(|entry| {
            let pair = entry.as_array()?;
            let cents = pair
                .first()
                .and_then(|p| p.as_f64().or_else(|| p.as_i64().map(|i| i as f64)))?;
            let qty = pair
                .get(1)
                .and_then(|q| q.as_f64().or_else(|| q.as_i64().map(|i| i as f64)))
                .unwrap_or(0.0);
            let price = cents / 100.0; // Kalshi uses cents
            if price > 0.0 {
                Some((price, qty))
            } else {
                None
            }
        })
        .collect();

    // Sort descending by price (highest first — same as bid order)
    levels.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
    levels
}
