use barter_data::event::DataKind;
use chrono::{DateTime, Utc};
use sqlx::PgPool;
use swym_dal::repo::market_event::{InsertOrderBookL1, InsertTrade};
use tracing::{error, info};

/// A market event with the DB instrument_id already resolved.
pub struct ResolvedEvent {
    pub instrument_id: i32,
    pub time_exchange: DateTime<Utc>,
    pub time_received: DateTime<Utc>,
    pub kind: DataKind,
}

pub struct IngestBuffer {
    pool: PgPool,
    trade_buf: Vec<InsertTrade>,
    l1_buf: Vec<InsertOrderBookL1>,
    batch_size: usize,
}

impl IngestBuffer {
    pub fn new(pool: PgPool, batch_size: usize) -> Self {
        Self {
            pool,
            trade_buf: Vec::with_capacity(batch_size),
            l1_buf: Vec::with_capacity(batch_size),
            batch_size,
        }
    }

    /// Buffer a resolved event. Returns true if a flush threshold was reached.
    pub fn push(&mut self, event: &ResolvedEvent) -> bool {
        match &event.kind {
            DataKind::Trade(trade) => {
                self.trade_buf.push(InsertTrade {
                    instrument_id: event.instrument_id,
                    time_exchange: event.time_exchange,
                    time_received: event.time_received,
                    trade_id: trade.id.clone(),
                    price: trade.price,
                    amount: trade.amount,
                    side: format!("{:?}", trade.side),
                });
            }
            DataKind::OrderBookL1(l1) => {
                let (bid_price, bid_amount) = l1
                    .best_bid
                    .map(|l| (Some(l.price), Some(l.amount)))
                    .unwrap_or((None, None));
                let (ask_price, ask_amount) = l1
                    .best_ask
                    .map(|l| (Some(l.price), Some(l.amount)))
                    .unwrap_or((None, None));

                self.l1_buf.push(InsertOrderBookL1 {
                    instrument_id: event.instrument_id,
                    time_exchange: event.time_exchange,
                    time_received: event.time_received,
                    best_bid_price: bid_price,
                    best_bid_amount: bid_amount,
                    best_ask_price: ask_price,
                    best_ask_amount: ask_amount,
                });
            }
            _ => {}
        }

        self.trade_buf.len() >= self.batch_size || self.l1_buf.len() >= self.batch_size
    }

    /// Flush all buffered events to the database.
    pub async fn flush(&mut self) {
        let trade_count = self.trade_buf.len();
        let l1_count = self.l1_buf.len();

        if trade_count > 0 {
            let trades: Vec<_> = self.trade_buf.drain(..).collect();
            if let Err(e) =
                swym_dal::repo::market_event::batch_insert_trades(&self.pool, &trades).await
            {
                error!(?e, "failed to flush trades");
            } else {
                info!(count = trade_count, "flushed trades");
            }
        }

        if l1_count > 0 {
            let snapshots: Vec<_> = self.l1_buf.drain(..).collect();
            if let Err(e) =
                swym_dal::repo::market_event::batch_insert_order_book_l1(&self.pool, &snapshots)
                    .await
            {
                error!(?e, "failed to flush L1 snapshots");
            } else {
                info!(count = l1_count, "flushed L1 snapshots");
            }
        }
    }

    pub fn is_empty(&self) -> bool {
        self.trade_buf.is_empty() && self.l1_buf.is_empty()
    }
}
