//! Polymarket orderbook WebSocket feed.
//!
//! Connects to Polymarket's CLOB WebSocket and streams
//! best bid/ask + mid price into a shared FeedSnapshot,
//! plus full L2 depth into a shared OrderbookSnapshot.

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

const POLYMARKET_WS_URL: &str = "wss://ws-subscriptions-clob.polymarket.com/ws/market";

/// Run the Polymarket orderbook feed until shutdown is signaled.
pub async fn run_polymarket_book(
    name: String,
    market_slug: String,
    snapshots: Arc<DashMap<String, FeedSnapshot>>,
    orderbooks: OrderbookStore,
    metrics: MetricsStore,
    global_shutdown: Arc<tokio::sync::Notify>,
    feed_shutdown: Arc<tokio::sync::Notify>,
) {
    // Pre-compute source string to avoid allocation per message
    let source = format!("polymarket:{}", market_slug);
    let mut reconnect_delay_ms: u64 = 1000;

    loop {
        let connect_result = tokio::select! {
            _ = global_shutdown.notified() => return,
            _ = feed_shutdown.notified() => return,
            result = connect_async(POLYMARKET_WS_URL) => result,
        };

        match connect_result {
            Ok((ws_stream, _)) => {
                reconnect_delay_ms = 1000; // Reset on successful connect
                let (mut write, mut read) = ws_stream.split();

                // Subscribe to the market's orderbook channel (re-subscribes on every reconnect)
                let subscribe_msg = serde_json::json!({
                    "type": "subscribe",
                    "channel": "market",
                    "assets_id": market_slug,
                });

                if let Err(e) = write
                    .send(tokio_tungstenite::tungstenite::Message::Text(
                        subscribe_msg.to_string(),
                    ))
                    .await
                {
                    warn!(
                        market = %market_slug,
                        error = %e,
                        "polymarket feed: subscribe failed"
                    );
                    if let Some(mut m) = metrics.get_mut(&name) {
                        m.error_count += 1;
                        m.last_error = e.to_string();
                    }
                    // Fall through to reconnect
                } else {
                    debug!(market = %market_slug, "polymarket feed: subscribed");
                    if let Some(mut m) = metrics.get_mut(&name) {
                        m.is_connected = true;
                    }

                    loop {
                        let msg = tokio::select! {
                            _ = global_shutdown.notified() => return,
            _ = feed_shutdown.notified() => return,
                            msg = read.next() => msg,
                        };

                        match msg {
                            Some(Ok(tokio_tungstenite::tungstenite::Message::Text(text))) => {
                                if let Ok(json) =
                                    serde_json::from_str::<serde_json::Value>(&text)
                                {
                                    // Extract all levels for L2 depth
                                    let bid_levels = extract_all_levels(&json, "bids");
                                    let ask_levels = extract_all_levels(&json, "asks");

                                    // Fallback: try nested "market" object
                                    let bid_levels = if bid_levels.is_empty() {
                                        json.get("market")
                                            .map(|m| extract_all_levels(m, "bids"))
                                            .unwrap_or_default()
                                    } else {
                                        bid_levels
                                    };
                                    let ask_levels = if ask_levels.is_empty() {
                                        json.get("market")
                                            .map(|m| extract_all_levels(m, "asks"))
                                            .unwrap_or_default()
                                    } else {
                                        ask_levels
                                    };

                                    let best_bid = bid_levels.first().map(|&(p, _)| p).unwrap_or(0.0);
                                    let best_ask = ask_levels.first().map(|&(p, _)| p).unwrap_or(0.0);

                                    // Also check for a direct price field
                                    let direct_price = json
                                        .get("price")
                                        .or_else(|| json.get("last_trade_price"))
                                        .and_then(|v| {
                                            v.as_f64()
                                                .or_else(|| v.as_str().and_then(|s| s.parse().ok()))
                                        });

                                    let has_book = best_bid > 0.0 || best_ask > 0.0;
                                    let has_price = direct_price.is_some();

                                    if has_book || has_price {
                                        let mid = if best_bid > 0.0 && best_ask > 0.0 {
                                            (best_bid + best_ask) / 2.0
                                        } else if let Some(p) = direct_price {
                                            p
                                        } else if best_bid > 0.0 {
                                            best_bid
                                        } else {
                                            best_ask
                                        };

                                        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: if best_bid > 0.0 {
                                                best_bid
                                            } else {
                                                direct_price.unwrap_or(0.0)
                                            },
                                            ask: if best_ask > 0.0 {
                                                best_ask
                                            } else {
                                                direct_price.unwrap_or(0.0)
                                            },
                                            volume_24h: 0.0,
                                            last_trade_size: 0.0,
                                            last_trade_is_buy: false,
                                        };

                                        snapshots.insert(name.clone(), snap);
                                        if let Some(mut m) = metrics.get_mut(&name) {
                                            m.update_count += 1;
                                            m.last_update_time = now;
                                        }

                                        // Store L2 orderbook if we have depth data
                                        if has_book {
                                            let book = OrderbookSnapshot::from_levels(
                                                bid_levels,
                                                ask_levels,
                                                now,
                                                source.clone(),
                                            );
                                            orderbooks.insert(name.clone(), book);
                                        }
                                    }
                                }
                            }
                            Some(Ok(tokio_tungstenite::tungstenite::Message::Close(_))) => break,
                            Some(Err(e)) => {
                                warn!(
                                    market = %market_slug,
                                    error = %e,
                                    "polymarket feed: connection error"
                                );
                                if let Some(mut m) = metrics.get_mut(&name) {
                                    m.error_count += 1;
                                    m.last_error = e.to_string();
                                    m.is_connected = false;
                                }
                                break;
                            }
                            None => break,
                            _ => {} // Ping/Pong/Binary — ignore
                        }
                    }
                }
            }
            Err(e) => {
                warn!(
                    market = %market_slug,
                    error = %e,
                    "polymarket feed: connection failed"
                );
                if let Some(mut m) = metrics.get_mut(&name) {
                    m.error_count += 1;
                    m.last_error = e.to_string();
                }
            }
        }

        // Reconnect with exponential backoff (1s, 2s, 4s, 8s, ... max 30s)
        if let Some(mut m) = metrics.get_mut(&name) {
            m.reconnect_count += 1;
            m.is_connected = false;
        }
        warn!(market = %market_slug, delay_ms = reconnect_delay_ms, "polymarket feed: reconnecting");
        tokio::select! {
            _ = global_shutdown.notified() => return,
            _ = feed_shutdown.notified() => return,
            _ = tokio::time::sleep(tokio::time::Duration::from_millis(reconnect_delay_ms)) => {},
        }
        reconnect_delay_ms = (reconnect_delay_ms * 2).min(30_000);
    }
}

/// Extract all price levels from an orderbook side.
/// Returns Vec<(price, size)>, sorted descending for bids, ascending for asks.
fn extract_all_levels(json: &serde_json::Value, side: &str) -> Vec<(f64, f64)> {
    let arr = match json.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| {
            // Format 1: {"price": "0.55", "size": "100"}
            let price = entry
                .get("price")
                .and_then(|p| {
                    p.as_f64()
                        .or_else(|| p.as_str().and_then(|s| s.parse::<f64>().ok()))
                })
                .or_else(|| {
                    // Format 2: [price, size]
                    entry
                        .as_array()
                        .and_then(|pair| pair.first())
                        .and_then(|p| {
                            p.as_f64()
                                .or_else(|| p.as_str().and_then(|s| s.parse::<f64>().ok()))
                        })
                })?;

            let size = entry
                .get("size")
                .and_then(|s| {
                    s.as_f64()
                        .or_else(|| s.as_str().and_then(|s| s.parse::<f64>().ok()))
                })
                .or_else(|| {
                    entry
                        .as_array()
                        .and_then(|pair| pair.get(1))
                        .and_then(|s| {
                            s.as_f64()
                                .or_else(|| s.as_str().and_then(|s| s.parse::<f64>().ok()))
                        })
                })
                .unwrap_or(0.0);

            if price > 0.0 {
                Some((price, size))
            } else {
                None
            }
        })
        .collect();

    // Sort: bids descending, asks ascending
    if side == "bids" {
        levels.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
    } else {
        levels.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
    }

    levels
}
