use chrono::{TimeZone, Utc};
use futures_util::{SinkExt, StreamExt};
use serde_json::json;
use tokio::sync::broadcast;
use tokio_tungstenite::{connect_async, tungstenite::Message};
use tracing::{debug, error, info, warn};
use uforia_ms_types::{Asset, CmError, MarketEvent, Venue};

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

/// Bybit spot WebSocket feed for trades and orderbook snapshots.
///
/// Subscribes to:
/// - `publicTrade.{SYMBOL}` - individual trades
/// - `orderbook.50.{SYMBOL}` - top-10 orderbook snapshots
pub struct BybitSpotFeed {
    ws_url: String,
    assets: Vec<Asset>,
    market_tx: broadcast::Sender<MarketEvent>,
    reconnect_base_ms: u64,
    reconnect_max_ms: u64,
}

impl BybitSpotFeed {
    pub fn new(
        ws_url: String,
        assets: Vec<Asset>,
        market_tx: broadcast::Sender<MarketEvent>,
        reconnect_base_ms: u64,
        reconnect_max_ms: u64,
    ) -> Self {
        Self {
            ws_url,
            assets,
            market_tx,
            reconnect_base_ms,
            reconnect_max_ms,
        }
    }

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

    /// Build the subscribe message for Bybit v5 API.
    fn subscribe_message(&self) -> String {
        let mut args = Vec::new();
        for asset in &self.assets {
            let sym = asset.symbol(Venue::BybitSpot);
            args.push(format!("publicTrade.{}", sym));
            args.push(format!("orderbook.50.{}", sym));
        }

        json!({
            "op": "subscribe",
            "args": args
        })
        .to_string()
    }

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

        loop {
            match connect_async(&self.ws_url).await {
                Ok((ws_stream, _)) => {
                    policy.reset();
                    info!("bybit_spot connected");

                    let (mut write, mut read) = ws_stream.split();

                    // Send subscribe message
                    let sub_msg = self.subscribe_message();
                    if let Err(e) = write.send(Message::Text(sub_msg.into())).await {
                        error!(error = %e, "bybit_spot failed to subscribe");
                        continue;
                    }
                    info!("bybit_spot subscribed");

                    // Bybit requires periodic pings
                    let mut ping_interval =
                        tokio::time::interval(std::time::Duration::from_secs(20));

                    loop {
                        tokio::select! {
                            msg = read.next() => {
                                match msg {
                                    Some(Ok(Message::Text(text))) => {
                                        if let Err(e) = self.handle_message(&text) {
                                            debug!(error = %e, "bybit_spot parse error");
                                        }
                                    }
                                    Some(Ok(Message::Ping(data))) => {
                                        let _ = write.send(Message::Pong(data)).await;
                                    }
                                    Some(Ok(Message::Close(_))) => {
                                        warn!("bybit_spot received close frame");
                                        break;
                                    }
                                    Some(Err(e)) => {
                                        error!(error = %e, "bybit_spot ws error");
                                        break;
                                    }
                                    None => {
                                        warn!("bybit_spot stream ended");
                                        break;
                                    }
                                    _ => {}
                                }
                            }
                            _ = ping_interval.tick() => {
                                let ping = json!({"op": "ping"}).to_string();
                                if let Err(e) = write.send(Message::Text(ping.into())).await {
                                    error!(error = %e, "bybit_spot ping failed");
                                    break;
                                }
                            }
                            _ = shutdown.changed() => {
                                if *shutdown.borrow() {
                                    info!("bybit_spot shutting down");
                                    return Ok(());
                                }
                            }
                        }
                    }
                }
                Err(e) => {
                    error!(error = %e, "bybit_spot connection failed");
                }
            }

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

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

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

        // Skip subscribe confirmations and pong responses
        if msg.get("op").is_some() {
            let success = msg
                .get("success")
                .and_then(|v| v.as_bool())
                .unwrap_or(false);
            if !success {
                warn!(msg = %text, "bybit_spot subscription failed");
            }
            return Ok(());
        }

        let topic = msg.get("topic").and_then(|v| v.as_str()).unwrap_or("");

        let data = msg.get("data");

        if topic.starts_with("publicTrade.") {
            self.handle_trades(topic, data)?;
        } else if topic.starts_with("orderbook.50.") {
            self.handle_orderbook(topic, data, &msg)?;
        }

        Ok(())
    }

    fn handle_trades(&self, topic: &str, data: Option<&serde_json::Value>) -> Result<(), CmError> {
        let symbol = topic.strip_prefix("publicTrade.").unwrap_or("");
        let asset = match Self::resolve_asset(symbol) {
            Some(a) => a,
            None => return Ok(()),
        };

        let trades = data
            .and_then(|v| v.as_array())
            .ok_or_else(|| CmError::Parse {
                context: "bybit_spot_trade".into(),
                msg: "missing or non-array 'data'".into(),
            })?;

        for raw_trade in trades {
            let trade = normalizer::parse_bybit_trade(raw_trade, asset, Venue::BybitSpot)?;
            let _ = self.market_tx.send(MarketEvent::Trade(trade));
        }

        Ok(())
    }

    fn handle_orderbook(
        &self,
        topic: &str,
        data: Option<&serde_json::Value>,
        msg: &serde_json::Value,
    ) -> Result<(), CmError> {
        let symbol = topic.strip_prefix("orderbook.50.").unwrap_or("");
        let asset = match Self::resolve_asset(symbol) {
            Some(a) => a,
            None => return Ok(()),
        };

        let book_data = data.ok_or_else(|| CmError::Parse {
            context: "bybit_spot_book".into(),
            msg: "missing 'data'".into(),
        })?;

        let ts_ms = msg
            .get("ts")
            .and_then(|v| v.as_i64())
            .unwrap_or_else(|| Utc::now().timestamp_millis());

        let timestamp = Utc
            .timestamp_millis_opt(ts_ms)
            .single()
            .unwrap_or_else(Utc::now);

        let snapshot =
            normalizer::parse_bybit_orderbook(book_data, asset, Venue::BybitSpot, timestamp)?;
        let _ = self.market_tx.send(MarketEvent::L2Snapshot(snapshot));

        Ok(())
    }
}

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

    fn make_feed() -> (BybitSpotFeed, broadcast::Receiver<MarketEvent>) {
        let (tx, rx) = broadcast::channel(1024);
        let feed = BybitSpotFeed::new(
            "wss://stream.bybit.com/v5/public/spot".into(),
            vec![Asset::Btc, Asset::Eth],
            tx,
            1000,
            60000,
        );
        (feed, rx)
    }

    #[test]
    fn test_subscribe_message() {
        let (feed, _) = make_feed();
        let sub: serde_json::Value = serde_json::from_str(&feed.subscribe_message()).unwrap();
        let args = sub["args"].as_array().unwrap();
        assert!(args.contains(&json!("publicTrade.BTCUSDT")));
        assert!(args.contains(&json!("publicTrade.ETHUSDT")));
        assert!(args.contains(&json!("orderbook.50.BTCUSDT")));
        assert!(args.contains(&json!("orderbook.50.ETHUSDT")));
    }

    #[test]
    fn test_handle_trade_message() {
        let (feed, mut rx) = make_feed();

        let msg = json!({
            "topic": "publicTrade.BTCUSDT",
            "type": "snapshot",
            "ts": 1672515782136i64,
            "data": [
                {
                    "T": 1672515782136i64,
                    "s": "BTCUSDT",
                    "S": "Buy",
                    "v": "0.001",
                    "p": "50000.00",
                    "i": "trade-123"
                }
            ]
        });

        feed.handle_message(&msg.to_string()).unwrap();
        let event = rx.try_recv().unwrap();
        match event {
            MarketEvent::Trade(trade) => {
                assert_eq!(trade.asset, Asset::Btc);
                assert_eq!(trade.venue, Venue::BybitSpot);
                assert!((trade.price - 50000.0).abs() < 1e-10);
                assert_eq!(trade.side, uforia_ms_types::Side::Buy);
            }
            _ => panic!("expected Trade event"),
        }
    }

    #[test]
    fn test_handle_orderbook_message() {
        let (feed, mut rx) = make_feed();

        let msg = json!({
            "topic": "orderbook.50.BTCUSDT",
            "type": "snapshot",
            "ts": 1672515782136i64,
            "data": {
                "s": "BTCUSDT",
                "b": [["50000.00", "1.5"], ["49999.00", "0.5"]],
                "a": [["50001.00", "1.0"], ["50002.00", "0.3"]],
                "u": 12345u64,
                "seq": 67890u64
            }
        });

        feed.handle_message(&msg.to_string()).unwrap();
        let event = rx.try_recv().unwrap();
        match event {
            MarketEvent::L2Snapshot(snap) => {
                assert_eq!(snap.asset, Asset::Btc);
                assert_eq!(snap.venue, Venue::BybitSpot);
                assert_eq!(snap.bids.len(), 2);
                assert_eq!(snap.asks.len(), 2);
            }
            _ => panic!("expected L2Snapshot event"),
        }
    }

    #[test]
    fn test_handle_subscribe_response() {
        let (feed, mut rx) = make_feed();

        let msg = json!({
            "op": "subscribe",
            "success": true,
            "req_id": "1",
            "conn_id": "abc123"
        });

        // Should not error and should not produce events
        feed.handle_message(&msg.to_string()).unwrap();
        assert!(rx.try_recv().is_err());
    }

    #[test]
    fn test_handle_pong_response() {
        let (feed, mut rx) = make_feed();

        let msg = json!({
            "op": "pong",
            "success": true,
            "args": ["1672515782136"]
        });

        feed.handle_message(&msg.to_string()).unwrap();
        assert!(rx.try_recv().is_err());
    }

    #[test]
    fn test_handle_multiple_trades() {
        let (feed, mut rx) = make_feed();

        let msg = json!({
            "topic": "publicTrade.ETHUSDT",
            "type": "snapshot",
            "ts": 1672515782136i64,
            "data": [
                {
                    "T": 1672515782136i64,
                    "s": "ETHUSDT",
                    "S": "Buy",
                    "v": "1.0",
                    "p": "3000.00",
                    "i": "t1"
                },
                {
                    "T": 1672515782137i64,
                    "s": "ETHUSDT",
                    "S": "Sell",
                    "v": "0.5",
                    "p": "3001.00",
                    "i": "t2"
                }
            ]
        });

        feed.handle_message(&msg.to_string()).unwrap();

        let e1 = rx.try_recv().unwrap();
        let e2 = rx.try_recv().unwrap();
        match (e1, e2) {
            (MarketEvent::Trade(t1), MarketEvent::Trade(t2)) => {
                assert!((t1.price - 3000.0).abs() < 1e-10);
                assert!((t2.price - 3001.0).abs() < 1e-10);
            }
            _ => panic!("expected two Trade events"),
        }
    }
}
