use std::collections::HashMap;
use std::sync::Arc;

use chrono::Utc;
use futures_util::StreamExt;
use ordered_float::OrderedFloat;
use reqwest::Client;
use serde_json::Value;
use tokio::sync::{broadcast, watch, Mutex};
use tokio_tungstenite::{connect_async, tungstenite::Message};
use tracing::{debug, error, info, warn};
use uforia_ms_types::{Asset, BookLevel, BookStatus, CmError, L2Snapshot, L2Update, MarketEvent, Venue};

use crate::normalizer;
use crate::reconnect::ReconnectPolicy;

#[derive(Debug, Default, Clone)]
struct BootstrapState {
    seeded: bool,
    last_update_id: Option<u64>,
    buffered: Vec<L2Update>,
    reseed_count: u64,
}

/// Binance spot WebSocket + REST bootstrap feed.
pub struct BinanceSpotFeed {
    ws_url: String,
    rest_url: String,
    assets: Vec<Asset>,
    market_tx: broadcast::Sender<MarketEvent>,
    reconnect_base_ms: u64,
    reconnect_max_ms: u64,
    client: Client,
    bootstrap: Arc<Mutex<HashMap<Asset, BootstrapState>>>,
}

impl BinanceSpotFeed {
    pub fn new(
        ws_url: String,
        rest_url: String,
        assets: Vec<Asset>,
        market_tx: broadcast::Sender<MarketEvent>,
        reconnect_base_ms: u64,
        reconnect_max_ms: u64,
    ) -> Self {
        let bootstrap = assets
            .iter()
            .copied()
            .map(|asset| (asset, BootstrapState::default()))
            .collect();
        Self {
            ws_url,
            rest_url,
            assets,
            market_tx,
            reconnect_base_ms,
            reconnect_max_ms,
            client: Client::new(),
            bootstrap: Arc::new(Mutex::new(bootstrap)),
        }
    }

    fn build_url(&self) -> String {
        let streams: Vec<String> = self
            .assets
            .iter()
            .flat_map(|asset| {
                let sym = asset.symbol(Venue::BinanceSpot);
                vec![format!("{}@trade", sym), format!("{}@depth@100ms", sym)]
            })
            .collect();

        format!("{}/stream?streams={}", self.ws_url, streams.join("/"))
    }

    fn resolve_asset(symbol: &str) -> Option<Asset> {
        match symbol {
            "BTCUSDT" | "btcusdt" => Some(Asset::Btc),
            "ETHUSDT" | "ethusdt" => Some(Asset::Eth),
            _ => None,
        }
    }

    pub async fn run(&self, mut shutdown: watch::Receiver<bool>) -> Result<(), CmError> {
        let mut policy = ReconnectPolicy::new(self.reconnect_base_ms, self.reconnect_max_ms);
        let url = self.build_url();
        info!(url = %url, "binance_spot feed starting");

        loop {
            self.reset_bootstrap().await;
            match connect_async(&url).await {
                Ok((ws_stream, _)) => {
                    policy.reset();
                    info!("binance_spot connected");
                    for asset in &self.assets {
                        self.spawn_snapshot_seed(*asset);
                    }

                    let (_write, mut read) = ws_stream.split();
                    loop {
                        tokio::select! {
                            msg = read.next() => {
                                match msg {
                                    Some(Ok(Message::Text(text))) => {
                                        if let Err(e) = self.handle_message(&text).await {
                                            debug!(error = %e, "binance_spot parse error");
                                        }
                                    }
                                    Some(Ok(Message::Close(_))) => {
                                        warn!("binance_spot received close frame");
                                        break;
                                    }
                                    Some(Err(e)) => {
                                        error!(error = %e, "binance_spot ws error");
                                        break;
                                    }
                                    None => {
                                        warn!("binance_spot stream ended");
                                        break;
                                    }
                                    _ => {}
                                }
                            }
                            _ = shutdown.changed() => {
                                if *shutdown.borrow() {
                                    info!("binance_spot shutting down");
                                    return Ok(());
                                }
                            }
                        }
                    }
                }
                Err(e) => {
                    error!(error = %e, "binance_spot connection failed");
                }
            }

            if *shutdown.borrow() {
                info!("binance_spot shutting down (before reconnect)");
                return Ok(());
            }

            policy.wait("binance_spot").await;
        }
    }

    async fn handle_message(&self, text: &str) -> Result<(), CmError> {
        let wrapper: Value = serde_json::from_str(text).map_err(|e| CmError::Parse {
            context: "binance_spot".into(),
            msg: format!("JSON parse error: {}", e),
        })?;

        let data = if wrapper.get("stream").is_some() {
            wrapper.get("data").ok_or_else(|| CmError::Parse {
                context: "binance_spot".into(),
                msg: "missing 'data' in combined stream".into(),
            })?
        } else {
            &wrapper
        };

        match data.get("e").and_then(|v| v.as_str()).unwrap_or("") {
            "trade" => {
                let symbol = data.get("s").and_then(|v| v.as_str()).unwrap_or("");
                if let Some(asset) = Self::resolve_asset(symbol) {
                    let trade = normalizer::parse_binance_spot_trade(data, asset)?;
                    let _ = self.market_tx.send(MarketEvent::Trade(trade));
                }
            }
            "depthUpdate" => {
                let symbol = data.get("s").and_then(|v| v.as_str()).unwrap_or("");
                if let Some(asset) = Self::resolve_asset(symbol) {
                    let update = normalizer::parse_binance_depth_update(data, asset, Venue::BinanceSpot)?;
                    self.process_depth_update(asset, update).await;
                }
            }
            other => {
                debug!(event_type = other, "binance_spot unknown event type");
            }
        }

        Ok(())
    }

    async fn process_depth_update(&self, asset: Asset, update: L2Update) {
        let mut resend_snapshot = false;
        let mut emit_update = None;
        let mut emit_status = None;
        {
            let mut states = self.bootstrap.lock().await;
            let state = states.entry(asset).or_default();
            if !state.seeded {
                state.buffered.push(update);
                if state.buffered.len() > 4000 {
                    let keep_from = state.buffered.len().saturating_sub(2000);
                    state.buffered = state.buffered.split_off(keep_from);
                }
                return;
            }
            if let (Some(first), Some(last), Some(prev_last)) =
                (update.first_update_id, update.last_update_id, state.last_update_id)
            {
                if last <= prev_last {
                    return;
                }
                if first <= prev_last + 1 && last >= prev_last + 1 {
                    state.last_update_id = Some(last);
                    emit_update = Some(update);
                } else {
                    state.seeded = false;
                    state.last_update_id = None;
                    state.reseed_count += 1;
                    state.buffered.clear();
                    emit_status = Some(BookStatus {
                        timestamp: update.timestamp,
                        asset,
                        venue: Venue::BinanceSpot,
                        is_seeded: false,
                        is_sequence_valid: false,
                        is_gap_detected: true,
                        reseed_count: state.reseed_count,
                    });
                    resend_snapshot = true;
                }
            }
        }
        if let Some(status) = emit_status {
            let _ = self.market_tx.send(MarketEvent::BookStatus(status));
        }
        if let Some(valid_update) = emit_update {
            let _ = self.market_tx.send(MarketEvent::L2Update(valid_update));
        }
        if resend_snapshot {
            self.spawn_snapshot_seed(asset);
        }
    }

    fn spawn_snapshot_seed(&self, asset: Asset) {
        let rest_url = self.rest_url.clone();
        let client = self.client.clone();
        let market_tx = self.market_tx.clone();
        let bootstrap = self.bootstrap.clone();
        tokio::spawn(async move {
            match fetch_snapshot(&client, &rest_url, asset, Venue::BinanceSpot).await {
                Ok(snapshot) => {
                    let mut events = vec![MarketEvent::L2Snapshot(snapshot.clone())];
                    let status;
                    {
                        let mut states = bootstrap.lock().await;
                        let state = states.entry(asset).or_default();
                        let mut current = snapshot.last_update_id.unwrap_or_default();
                        let buffered = std::mem::take(&mut state.buffered);
                        state.seeded = true;
                        state.last_update_id = Some(current);
                        for update in buffered {
                            let first = update.first_update_id.unwrap_or_default();
                            let last = update.last_update_id.unwrap_or_default();
                            if last <= current {
                                continue;
                            }
                            if first <= current + 1 && last >= current + 1 {
                                current = last;
                                state.last_update_id = Some(current);
                                events.push(MarketEvent::L2Update(update));
                            }
                        }
                        status = Some(BookStatus {
                            timestamp: snapshot.timestamp,
                            asset,
                            venue: Venue::BinanceSpot,
                            is_seeded: true,
                            is_sequence_valid: true,
                            is_gap_detected: false,
                            reseed_count: state.reseed_count,
                        });
                    }
                    for event in events {
                        let _ = market_tx.send(event);
                    }
                    if let Some(book_status) = status {
                        let _ = market_tx.send(MarketEvent::BookStatus(book_status));
                    }
                }
                Err(error) => {
                    error!(asset = %asset, error = %error, "binance_spot snapshot bootstrap failed");
                }
            }
        });
    }

    async fn reset_bootstrap(&self) {
        let mut states = self.bootstrap.lock().await;
        for state in states.values_mut() {
            state.seeded = false;
            state.last_update_id = None;
            state.buffered.clear();
        }
    }
}

async fn fetch_snapshot(
    client: &Client,
    rest_url: &str,
    asset: Asset,
    venue: Venue,
) -> Result<L2Snapshot, CmError> {
    let symbol = asset.symbol(venue).to_uppercase();
    let response = client
        .get(format!("{rest_url}/api/v3/depth"))
        .query(&[("symbol", symbol.as_str()), ("limit", "1000")])
        .send()
        .await
        .map_err(|e| CmError::Feed {
            venue: venue.to_string(),
            msg: format!("binance spot snapshot request failed: {e}"),
        })?;
    let raw: Value = response
        .json::<Value>()
        .await
        .map_err(|e| CmError::Parse {
            context: "binance_spot_snapshot".into(),
            msg: format!("snapshot json parse error: {e}"),
        })?;
    let last_update_id = raw.get("lastUpdateId").and_then(Value::as_u64);
    let now = Utc::now();
    let bids = parse_levels(raw.get("bids"))?;
    let asks = parse_levels(raw.get("asks"))?;
    Ok(L2Snapshot {
        timestamp: now,
        asset,
        venue,
        bids,
        asks,
        last_update_id,
    })
}

fn parse_levels(raw: Option<&Value>) -> Result<Vec<BookLevel>, CmError> {
    let levels = raw.and_then(Value::as_array).ok_or_else(|| CmError::Parse {
        context: "binance_snapshot".into(),
        msg: "missing levels".into(),
    })?;
    levels
        .iter()
        .map(|entry| {
            let parts = entry.as_array().ok_or_else(|| CmError::Parse {
                context: "binance_snapshot".into(),
                msg: "level is not array".into(),
            })?;
            let price = parts.first().and_then(Value::as_str).ok_or_else(|| CmError::Parse {
                context: "binance_snapshot".into(),
                msg: "missing level price".into(),
            })?;
            let size = parts.get(1).and_then(Value::as_str).ok_or_else(|| CmError::Parse {
                context: "binance_snapshot".into(),
                msg: "missing level size".into(),
            })?;
            Ok(BookLevel {
                price: OrderedFloat(price.parse::<f64>().map_err(|e| CmError::Parse {
                    context: "binance_snapshot".into(),
                    msg: format!("invalid price: {e}"),
                })?),
                size: size.parse::<f64>().map_err(|e| CmError::Parse {
                    context: "binance_snapshot".into(),
                    msg: format!("invalid size: {e}"),
                })?,
            })
        })
        .collect()
}

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

    #[test]
    fn test_parse_levels() {
        let raw = json!([["50000.0", "1.0"], ["49999.5", "2.0"]]);
        let parsed = parse_levels(Some(&raw)).unwrap();
        assert_eq!(parsed.len(), 2);
        assert_eq!(parsed[0].price, OrderedFloat(50000.0));
    }

    #[test]
    fn test_build_url() {
        let (tx, _) = broadcast::channel(32);
        let feed = BinanceSpotFeed::new(
            "wss://stream.binance.com:9443".into(),
            "https://api.binance.com".into(),
            vec![Asset::Btc, Asset::Eth],
            tx,
            1000,
            60000,
        );
        let url = feed.build_url();
        assert!(url.contains("btcusdt@trade"));
        assert!(url.contains("ethusdt@depth@100ms"));
    }

    #[test]
    fn test_depth_update_parsing() {
        let raw = json!({
            "e": "depthUpdate",
            "E": 1672515782136i64,
            "s": "BTCUSDT",
            "U": 157u64,
            "u": 160u64,
            "b": [["50000.00", "1.5"]],
            "a": [["50001.00", "1.0"]]
        });
        let update = normalizer::parse_binance_depth_update(&raw, Asset::Btc, Venue::BinanceSpot).unwrap();
        assert_eq!(update.first_update_id, Some(157));
        assert_eq!(update.last_update_id, Some(160));
    }
}
