use crate::error::DalError;
use crate::models::market_event::{MarketCandleRow, MarketOrderBookL1Row, MarketTradeRow};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use sqlx::{PgPool, QueryBuilder};

/// A trade to be batch-inserted.
pub struct InsertTrade {
    pub instrument_id: i32,
    pub time_exchange: DateTime<Utc>,
    pub time_received: DateTime<Utc>,
    pub trade_id: String,
    pub price: f64,
    pub amount: f64,
    pub side: String,
}

/// An L1 order book snapshot to be batch-inserted.
pub struct InsertOrderBookL1 {
    pub instrument_id: i32,
    pub time_exchange: DateTime<Utc>,
    pub time_received: DateTime<Utc>,
    pub best_bid_price: Option<Decimal>,
    pub best_bid_amount: Option<Decimal>,
    pub best_ask_price: Option<Decimal>,
    pub best_ask_amount: Option<Decimal>,
}

/// Batch insert trades. No-op if `trades` is empty.
pub async fn batch_insert_trades(pool: &PgPool, trades: &[InsertTrade]) -> Result<(), DalError> {
    if trades.is_empty() {
        return Ok(());
    }

    let mut qb: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
        "INSERT INTO market_trades (instrument_id, time_exchange, time_received, trade_id, price, amount, side) ",
    );

    qb.push_values(trades, |mut b, t| {
        b.push_bind(t.instrument_id)
            .push_bind(t.time_exchange)
            .push_bind(t.time_received)
            .push_bind(&t.trade_id)
            .push_bind(t.price)
            .push_bind(t.amount)
            .push_bind(&t.side);
    });

    qb.push(" ON CONFLICT (instrument_id, trade_id) DO NOTHING");

    qb.build().execute(pool).await?;
    Ok(())
}

/// Batch insert L1 order book snapshots. No-op if `snapshots` is empty.
pub async fn batch_insert_order_book_l1(
    pool: &PgPool,
    snapshots: &[InsertOrderBookL1],
) -> Result<(), DalError> {
    if snapshots.is_empty() {
        return Ok(());
    }

    let mut qb: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
        "INSERT INTO market_order_book_l1 (instrument_id, time_exchange, time_received, best_bid_price, best_bid_amount, best_ask_price, best_ask_amount) ",
    );

    qb.push_values(snapshots, |mut b, s| {
        b.push_bind(s.instrument_id)
            .push_bind(s.time_exchange)
            .push_bind(s.time_received)
            .push_bind(s.best_bid_price)
            .push_bind(s.best_bid_amount)
            .push_bind(s.best_ask_price)
            .push_bind(s.best_ask_amount);
    });

    qb.build().execute(pool).await?;
    Ok(())
}

/// Query trades for an instrument within a time range.
pub async fn query_trades(
    pool: &PgPool,
    instrument_id: i32,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
    limit: i64,
) -> Result<Vec<MarketTradeRow>, DalError> {
    let rows = sqlx::query_as::<_, MarketTradeRow>(
        "SELECT id, instrument_id, time_exchange, time_received, trade_id, price, amount, side
         FROM market_trades
         WHERE instrument_id = $1 AND time_exchange >= $2 AND time_exchange < $3
         ORDER BY time_exchange
         LIMIT $4",
    )
    .bind(instrument_id)
    .bind(from)
    .bind(to)
    .bind(limit)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}

/// Load a page of trades for backtest replay, ordered by (time_exchange, id).
///
/// Use `last_id = 0` for the first page; pass the `id` of the last returned row as
/// `last_id` for subsequent pages.  Returns an empty Vec when all trades are exhausted.
pub async fn load_trades_page(
    pool: &PgPool,
    instrument_id: i32,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
    last_id: i64,
    page_size: i64,
) -> Result<Vec<MarketTradeRow>, DalError> {
    let rows = sqlx::query_as::<_, MarketTradeRow>(
        "SELECT id, instrument_id, time_exchange, time_received, trade_id, price, amount, side
         FROM market_trades
         WHERE instrument_id = $1
           AND time_exchange >= $2
           AND time_exchange < $3
           AND id > $4
         ORDER BY id
         LIMIT $5",
    )
    .bind(instrument_id)
    .bind(from)
    .bind(to)
    .bind(last_id)
    .bind(page_size)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}

/// Return the min and max trade timestamps for an instrument, or None if no data.
pub async fn trade_data_range(
    pool: &PgPool,
    instrument_id: i32,
) -> Result<Option<(DateTime<Utc>, DateTime<Utc>)>, DalError> {
    let row: Option<(Option<DateTime<Utc>>, Option<DateTime<Utc>>)> = sqlx::query_as(
        "SELECT MIN(time_exchange), MAX(time_exchange)
         FROM market_trades
         WHERE instrument_id = $1",
    )
    .bind(instrument_id)
    .fetch_optional(pool)
    .await?;

    Ok(row.and_then(|(min, max)| match (min, max) {
        (Some(a), Some(b)) => Some((a, b)),
        _ => None,
    }))
}

/// Count trades for an instrument within a time range.
pub async fn count_trades(
    pool: &PgPool,
    instrument_id: i32,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
) -> Result<i64, DalError> {
    let (count,): (i64,) = sqlx::query_as(
        "SELECT COUNT(*) FROM market_trades
         WHERE instrument_id = $1 AND time_exchange >= $2 AND time_exchange < $3",
    )
    .bind(instrument_id)
    .bind(from)
    .bind(to)
    .fetch_one(pool)
    .await?;

    Ok(count)
}

/// A candle to be batch-inserted.
pub struct InsertCandle {
    pub instrument_id: i32,
    pub interval: String,
    pub open_time: DateTime<Utc>,
    pub close_time: DateTime<Utc>,
    pub open: f64,
    pub high: f64,
    pub low: f64,
    pub close: f64,
    pub volume: f64,
    pub trade_count: i32,
}

/// Batch insert candles. Skips duplicates (same instrument + interval + open_time).
pub async fn batch_insert_candles(
    pool: &PgPool,
    candles: &[InsertCandle],
) -> Result<u64, DalError> {
    if candles.is_empty() {
        return Ok(0);
    }

    let mut qb: QueryBuilder<sqlx::Postgres> = QueryBuilder::new(
        "INSERT INTO market_candles \
         (instrument_id, interval, open_time, close_time, open, high, low, close, volume, trade_count) ",
    );

    qb.push_values(candles, |mut b, c| {
        b.push_bind(c.instrument_id)
            .push_bind(&c.interval)
            .push_bind(c.open_time)
            .push_bind(c.close_time)
            .push_bind(c.open)
            .push_bind(c.high)
            .push_bind(c.low)
            .push_bind(c.close)
            .push_bind(c.volume)
            .push_bind(c.trade_count);
    });

    qb.push(" ON CONFLICT (instrument_id, interval, open_time) DO NOTHING");

    let result = qb.build().execute(pool).await?;
    Ok(result.rows_affected())
}

/// Load a page of candles for backtest replay, ordered by open_time.
///
/// Use `last_id = 0` for the first page; pass the `id` of the last returned row
/// for subsequent pages. Returns an empty Vec when all candles are exhausted.
pub async fn load_candles_page(
    pool: &PgPool,
    instrument_id: i32,
    interval: &str,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
    last_id: i64,
    page_size: i64,
) -> Result<Vec<MarketCandleRow>, DalError> {
    let rows = sqlx::query_as::<_, MarketCandleRow>(
        "SELECT id, instrument_id, interval, open_time, close_time,
                open, high, low, close, volume, trade_count
         FROM market_candles
         WHERE instrument_id = $1
           AND interval = $2
           AND open_time >= $3
           AND open_time < $4
           AND id > $5
         ORDER BY id
         LIMIT $6",
    )
    .bind(instrument_id)
    .bind(interval)
    .bind(from)
    .bind(to)
    .bind(last_id)
    .bind(page_size)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}

/// Load all candles for an instrument+interval within a time range, ordered by open_time.
pub async fn load_candles_range(
    pool: &PgPool,
    instrument_id: i32,
    interval: &str,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
) -> Result<Vec<MarketCandleRow>, DalError> {
    let rows = sqlx::query_as::<_, MarketCandleRow>(
        "SELECT id, instrument_id, interval, open_time, close_time,
                open, high, low, close, volume, trade_count
         FROM market_candles
         WHERE instrument_id = $1
           AND interval = $2
           AND open_time >= $3
           AND open_time < $4
         ORDER BY open_time",
    )
    .bind(instrument_id)
    .bind(interval)
    .bind(from)
    .bind(to)
    .fetch_all(pool)
    .await?;
    Ok(rows)
}

/// Return the min open_time and max close_time for candles of a given interval, or None.
pub async fn candle_data_range(
    pool: &PgPool,
    instrument_id: i32,
    interval: &str,
) -> Result<Option<(DateTime<Utc>, DateTime<Utc>)>, DalError> {
    let row: Option<(Option<DateTime<Utc>>, Option<DateTime<Utc>>)> = sqlx::query_as(
        "SELECT MIN(open_time), MAX(close_time)
         FROM market_candles
         WHERE instrument_id = $1 AND interval = $2",
    )
    .bind(instrument_id)
    .bind(interval)
    .fetch_optional(pool)
    .await?;

    Ok(row.and_then(|(min, max)| match (min, max) {
        (Some(a), Some(b)) => Some((a, b)),
        _ => None,
    }))
}

/// Count candles for an instrument + interval within a time range.
pub async fn count_candles(
    pool: &PgPool,
    instrument_id: i32,
    interval: &str,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
) -> Result<i64, DalError> {
    let (count,): (i64,) = sqlx::query_as(
        "SELECT COUNT(*) FROM market_candles
         WHERE instrument_id = $1 AND interval = $2
           AND open_time >= $3 AND open_time < $4",
    )
    .bind(instrument_id)
    .bind(interval)
    .bind(from)
    .bind(to)
    .fetch_one(pool)
    .await?;

    Ok(count)
}

/// Return available (interval, first_open_time, last_close_time, count) for an instrument.
pub async fn candle_coverage(
    pool: &PgPool,
    instrument_id: i32,
) -> Result<Vec<(String, DateTime<Utc>, DateTime<Utc>, i64)>, DalError> {
    let rows: Vec<(String, DateTime<Utc>, DateTime<Utc>, i64)> = sqlx::query_as(
        "SELECT interval, MIN(open_time), MAX(close_time), COUNT(*)
         FROM market_candles
         WHERE instrument_id = $1
         GROUP BY interval
         ORDER BY interval",
    )
    .bind(instrument_id)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}

/// Delete candles older than `cutoff` for the given instrument and interval.
pub async fn purge_old_candles(
    pool: &PgPool,
    instrument_id: i32,
    interval: &str,
    cutoff: DateTime<Utc>,
) -> Result<u64, DalError> {
    let result = sqlx::query(
        "DELETE FROM market_candles
         WHERE instrument_id = $1 AND interval = $2 AND open_time < $3",
    )
    .bind(instrument_id)
    .bind(interval)
    .bind(cutoff)
    .execute(pool)
    .await?;
    Ok(result.rows_affected())
}

/// Query L1 order book snapshots for an instrument within a time range.
pub async fn query_order_book_l1(
    pool: &PgPool,
    instrument_id: i32,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
    limit: i64,
) -> Result<Vec<MarketOrderBookL1Row>, DalError> {
    let rows = sqlx::query_as::<_, MarketOrderBookL1Row>(
        "SELECT id, instrument_id, time_exchange, time_received,
                best_bid_price, best_bid_amount, best_ask_price, best_ask_amount
         FROM market_order_book_l1
         WHERE instrument_id = $1 AND time_exchange >= $2 AND time_exchange < $3
         ORDER BY time_exchange
         LIMIT $4",
    )
    .bind(instrument_id)
    .bind(from)
    .bind(to)
    .bind(limit)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}
