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, FundingEvent, FundingRate, Liquidation, MarketEvent, OpenInterest, Side, Venue,
};

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

/// Bybit perpetual futures WebSocket feed.
///
/// Subscribes to:
/// - `publicTrade.{SYMBOL}` - trades
/// - `orderbook.50.{SYMBOL}` - top-10 orderbook
/// - `tickers.{SYMBOL}` - OI, funding rate, mark price
/// - `allLiquidation.{SYMBOL}` - liquidation events
pub struct BybitPerpsFeed {
    ws_url: String,
    assets: Vec<Asset>,
    market_tx: broadcast::Sender<MarketEvent>,
    funding_tx: broadcast::Sender<FundingEvent>,
    reconnect_base_ms: u64,
    reconnect_max_ms: u64,
}

impl BybitPerpsFeed {
    pub fn new(
        ws_url: String,
        assets: Vec<Asset>,
        market_tx: broadcast::Sender<MarketEvent>,
        funding_tx: broadcast::Sender<FundingEvent>,
        reconnect_base_ms: u64,
        reconnect_max_ms: u64,
    ) -> Self {
        Self {
            ws_url,
            assets,
            market_tx,
            funding_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,
        }
    }

    fn subscribe_message(&self) -> String {
        let mut args = Vec::new();
        for asset in &self.assets {
            let sym = asset.symbol(Venue::BybitPerps);
            args.push(format!("publicTrade.{}", sym));
            args.push(format!("orderbook.50.{}", sym));
            args.push(format!("tickers.{}", sym));
            args.push(format!("allLiquidation.{}", 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_perps feed starting");

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

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

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

                    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_perps parse error");
                                        }
                                    }
                                    Some(Ok(Message::Ping(data))) => {
                                        let _ = write.send(Message::Pong(data)).await;
                                    }
                                    Some(Ok(Message::Close(_))) => {
                                        warn!("bybit_perps received close frame");
                                        break;
                                    }
                                    Some(Err(e)) => {
                                        error!(error = %e, "bybit_perps ws error");
                                        break;
                                    }
                                    None => {
                                        warn!("bybit_perps 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_perps ping failed");
                                    break;
                                }
                            }
                            _ = shutdown.changed() => {
                                if *shutdown.borrow() {
                                    info!("bybit_perps shutting down");
                                    return Ok(());
                                }
                            }
                        }
                    }
                }
                Err(e) => {
                    error!(error = %e, "bybit_perps connection failed");
                }
            }

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

            policy.wait("bybit_perps").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_perps".into(),
            msg: format!("JSON parse error: {}", e),
        })?;

        // Skip op responses (subscribe, pong)
        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_perps operation 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)?;
        } else if topic.starts_with("tickers.") {
            self.handle_tickers(topic, data, &msg)?;
        } else if topic.starts_with("allLiquidation.") {
            self.handle_liquidation(topic, data)?;
        }

        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_perps_trade".into(),
                msg: "missing or non-array 'data'".into(),
            })?;

        for raw_trade in trades {
            let trade = normalizer::parse_bybit_trade(raw_trade, asset, Venue::BybitPerps)?;
            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_perps_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::BybitPerps, timestamp)?;
        let _ = self.market_tx.send(MarketEvent::L2Snapshot(snapshot));

        Ok(())
    }

    /// Handle tickers topic which contains OI and funding rate info.
    ///
    /// Bybit v5 tickers message:
    /// ```json
    /// {
    ///   "topic": "tickers.BTCUSDT",
    ///   "type": "snapshot",
    ///   "cs": 12345,
    ///   "ts": 1672515782136,
    ///   "data": {
    ///     "symbol": "BTCUSDT",
    ///     "openInterest": "50000.00",
    ///     "openInterestValue": "2500000000.00",
    ///     "fundingRate": "0.0001",
    ///     "nextFundingTime": "1672531200000",
    ///     "markPrice": "50000.00",
    ///     "indexPrice": "49999.50",
    ///     ...
    ///   }
    /// }
    /// ```
    fn handle_tickers(
        &self,
        topic: &str,
        data: Option<&serde_json::Value>,
        msg: &serde_json::Value,
    ) -> Result<(), CmError> {
        let symbol = topic.strip_prefix("tickers.").unwrap_or("");
        let asset = match Self::resolve_asset(symbol) {
            Some(a) => a,
            None => return Ok(()),
        };

        let ticker = data.ok_or_else(|| CmError::Parse {
            context: "bybit_perps_ticker".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);

        // Open Interest
        if let Some(oi_str) = ticker.get("openInterest").and_then(|v| v.as_str()) {
            if let Ok(oi) = oi_str.parse::<f64>() {
                let oi_value: f64 = ticker
                    .get("openInterestValue")
                    .and_then(|v| v.as_str())
                    .and_then(|s| s.parse().ok())
                    .unwrap_or(0.0);

                let _ = self.funding_tx.send(FundingEvent::Oi(OpenInterest {
                    timestamp,
                    asset,
                    venue: Venue::BybitPerps,
                    oi,
                    oi_value,
                }));
            }
        }

        // Funding Rate
        if let Some(rate_str) = ticker.get("fundingRate").and_then(|v| v.as_str()) {
            if let Ok(rate) = rate_str.parse::<f64>() {
                let next_funding_time = ticker
                    .get("nextFundingTime")
                    .and_then(|v| v.as_str())
                    .and_then(|s| s.parse::<i64>().ok())
                    .and_then(|ms| Utc.timestamp_millis_opt(ms).single());

                let _ = self.funding_tx.send(FundingEvent::Funding(FundingRate {
                    timestamp,
                    asset,
                    venue: Venue::BybitPerps,
                    rate,
                    next_funding_time,
                }));
            }
        }

        // Mark/Index price
        let mark_price: Option<f64> = ticker
            .get("markPrice")
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse().ok());
        let index_price: Option<f64> = ticker
            .get("indexPrice")
            .and_then(|v| v.as_str())
            .and_then(|s| s.parse().ok());

        if let (Some(mp), Some(ip)) = (mark_price, index_price) {
            let _ = self
                .market_tx
                .send(MarketEvent::MarkIndex(uforia_ms_types::MarkIndex {
                    timestamp,
                    asset,
                    venue: Venue::BybitPerps,
                    mark_price: mp,
                    index_price: ip,
                }));
        }

        Ok(())
    }

    /// Handle liquidation events.
    ///
    /// ```json
    /// {
    ///   "topic": "allLiquidation.BTCUSDT",
    ///   "type": "snapshot",
    ///   "ts": 1672515782136,
    ///   "data": {
    ///     "symbol": "BTCUSDT",
    ///     "side": "Buy",
    ///     "size": "0.01",
    ///     "price": "49000.00",
    ///     "updatedTime": 1672515782136
    ///   }
    /// }
    /// ```
    fn handle_liquidation(
        &self,
        topic: &str,
        data: Option<&serde_json::Value>,
    ) -> Result<(), CmError> {
        let symbol = topic.strip_prefix("allLiquidation.").unwrap_or("");
        let asset = match Self::resolve_asset(symbol) {
            Some(a) => a,
            None => return Ok(()),
        };

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

        let ts_ms = liq_data
            .get("updatedTime")
            .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 side_str = liq_data.get("side").and_then(|v| v.as_str()).unwrap_or("");
        let side = match side_str {
            "Buy" => Side::Buy,
            "Sell" => Side::Sell,
            other => {
                return Err(CmError::Parse {
                    context: "bybit_perps_liq".into(),
                    msg: format!("unknown side: {}", other),
                })
            }
        };

        let price: f64 = liq_data
            .get("price")
            .and_then(|v| v.as_str())
            .unwrap_or("0")
            .parse()
            .unwrap_or(0.0);

        let size: f64 = liq_data
            .get("size")
            .and_then(|v| v.as_str())
            .unwrap_or("0")
            .parse()
            .unwrap_or(0.0);

        let _ = self.funding_tx.send(FundingEvent::Liquidation(Liquidation {
            timestamp,
            asset,
            venue: Venue::BybitPerps,
            side,
            price,
            size,
        }));

        Ok(())
    }
}

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

    fn make_feed() -> (
        BybitPerpsFeed,
        broadcast::Receiver<MarketEvent>,
        broadcast::Receiver<FundingEvent>,
    ) {
        let (market_tx, market_rx) = broadcast::channel(1024);
        let (funding_tx, funding_rx) = broadcast::channel(1024);
        let feed = BybitPerpsFeed::new(
            "wss://stream.bybit.com/v5/public/linear".into(),
            vec![Asset::Btc, Asset::Eth],
            market_tx,
            funding_tx,
            1000,
            60000,
        );
        (feed, market_rx, funding_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!("orderbook.50.BTCUSDT")));
        assert!(args.contains(&json!("tickers.BTCUSDT")));
        assert!(args.contains(&json!("allLiquidation.BTCUSDT")));
        assert!(args.contains(&json!("publicTrade.ETHUSDT")));
    }

    #[test]
    fn test_handle_trade() {
        let (feed, mut market_rx, _) = make_feed();

        let msg = json!({
            "topic": "publicTrade.BTCUSDT",
            "type": "snapshot",
            "ts": 1672515782136i64,
            "data": [
                {
                    "T": 1672515782136i64,
                    "s": "BTCUSDT",
                    "S": "Sell",
                    "v": "0.01",
                    "p": "51000.00",
                    "i": "liq-1"
                }
            ]
        });

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

    #[test]
    fn test_handle_orderbook() {
        let (feed, mut market_rx, _) = make_feed();

        let msg = json!({
            "topic": "orderbook.50.ETHUSDT",
            "type": "snapshot",
            "ts": 1672515782136i64,
            "data": {
                "s": "ETHUSDT",
                "b": [["3000.00", "10.0"]],
                "a": [["3001.00", "5.0"]],
                "u": 100u64,
                "seq": 200u64
            }
        });

        feed.handle_message(&msg.to_string()).unwrap();
        let event = market_rx.try_recv().unwrap();
        match event {
            MarketEvent::L2Snapshot(snap) => {
                assert_eq!(snap.asset, Asset::Eth);
                assert_eq!(snap.venue, Venue::BybitPerps);
                assert_eq!(snap.bids.len(), 1);
            }
            _ => panic!("expected L2Snapshot"),
        }
    }

    #[test]
    fn test_handle_tickers_oi_and_funding() {
        let (feed, mut market_rx, mut funding_rx) = make_feed();

        let msg = json!({
            "topic": "tickers.BTCUSDT",
            "type": "snapshot",
            "cs": 12345u64,
            "ts": 1672515782136i64,
            "data": {
                "symbol": "BTCUSDT",
                "openInterest": "50000.00",
                "openInterestValue": "2500000000.00",
                "fundingRate": "0.0001",
                "nextFundingTime": "1672531200000",
                "markPrice": "50000.00",
                "indexPrice": "49999.50",
                "lastPrice": "50000.00",
                "bid1Price": "49999.00",
                "ask1Price": "50001.00"
            }
        });

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

        // Should emit OI event
        let oi_event = funding_rx.try_recv().unwrap();
        match oi_event {
            FundingEvent::Oi(oi) => {
                assert_eq!(oi.asset, Asset::Btc);
                assert!((oi.oi - 50000.0).abs() < 1e-10);
                assert!((oi.oi_value - 2_500_000_000.0).abs() < 1e-10);
            }
            _ => panic!("expected OI event"),
        }

        // Should emit Funding event
        let fr_event = funding_rx.try_recv().unwrap();
        match fr_event {
            FundingEvent::Funding(fr) => {
                assert_eq!(fr.asset, Asset::Btc);
                assert!((fr.rate - 0.0001).abs() < 1e-10);
                assert!(fr.next_funding_time.is_some());
            }
            _ => panic!("expected Funding event"),
        }

        // Should emit MarkIndex event
        let mark_event = market_rx.try_recv().unwrap();
        match mark_event {
            MarketEvent::MarkIndex(mi) => {
                assert!((mi.mark_price - 50000.0).abs() < 1e-10);
                assert!((mi.index_price - 49999.5).abs() < 1e-10);
            }
            _ => panic!("expected MarkIndex"),
        }
    }

    #[test]
    fn test_handle_liquidation() {
        let (feed, _, mut funding_rx) = make_feed();

        let msg = json!({
            "topic": "allLiquidation.BTCUSDT",
            "type": "snapshot",
            "ts": 1672515782136i64,
            "data": {
                "symbol": "BTCUSDT",
                "side": "Sell",
                "size": "0.5",
                "price": "48500.00",
                "updatedTime": 1672515782136i64
            }
        });

        feed.handle_message(&msg.to_string()).unwrap();
        let event = funding_rx.try_recv().unwrap();
        match event {
            FundingEvent::Liquidation(liq) => {
                assert_eq!(liq.asset, Asset::Btc);
                assert_eq!(liq.side, Side::Sell);
                assert!((liq.price - 48500.0).abs() < 1e-10);
                assert!((liq.size - 0.5).abs() < 1e-10);
            }
            _ => panic!("expected Liquidation"),
        }
    }

    #[test]
    fn test_handle_subscribe_ok() {
        let (feed, mut market_rx, _) = make_feed();

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

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