use crate::error::DalError;
use crate::models::paper_run_position::PaperRunPositionRow;
use rust_decimal::Decimal;
use sqlx::PgPool;
use uuid::Uuid;

pub struct InsertPosition {
    pub seq: i32,
    pub side: String,
    pub price_entry: Decimal,
    pub price_exit: Decimal,
    pub quantity: Decimal,
    pub pnl: Decimal,
    pub fees_enter: Decimal,
    pub fees_exit: Decimal,
    pub time_enter: chrono::DateTime<chrono::Utc>,
    pub time_exit: chrono::DateTime<chrono::Utc>,
}

/// Maximum rows per INSERT statement (11 params each; PostgreSQL limit is 65535 total).
const INSERT_CHUNK_SIZE: usize = 5_000;

/// Bulk-insert positions for a completed backtest run, chunked to stay within
/// PostgreSQL's 65535 bind-parameter limit.
pub async fn insert_batch(
    pool: &PgPool,
    run_id: Uuid,
    positions: &[InsertPosition],
) -> Result<(), DalError> {
    for chunk in positions.chunks(INSERT_CHUNK_SIZE) {
        let mut query = String::from(
            "INSERT INTO paper_run_positions \
             (run_id, seq, side, price_entry, price_exit, quantity, pnl, fees_enter, fees_exit, time_enter, time_exit) VALUES "
        );

        for i in 0..chunk.len() {
            let base = i * 11;
            if i > 0 {
                query.push(',');
            }
            query.push_str(&format!(
                "(${}, ${}, ${}, ${}, ${}, ${}, ${}, ${}, ${}, ${}, ${})",
                base + 1, base + 2, base + 3, base + 4, base + 5,
                base + 6, base + 7, base + 8, base + 9, base + 10, base + 11,
            ));
        }

        let mut q = sqlx::query(&query);
        for p in chunk {
            q = q
                .bind(run_id)
                .bind(p.seq)
                .bind(&p.side)
                .bind(p.price_entry)
                .bind(p.price_exit)
                .bind(p.quantity)
                .bind(p.pnl)
                .bind(p.fees_enter)
                .bind(p.fees_exit)
                .bind(p.time_enter)
                .bind(p.time_exit);
        }

        q.execute(pool).await?;
    }
    Ok(())
}

#[derive(Default)]
pub struct PositionAggregateStats {
    pub winning: i64,
    pub losing: i64,
    /// Net PnL (barter `pnl_realised` is already after fees).
    pub total_pnl_net: Decimal,
    pub total_fees: Decimal,
    /// Average PnL of winning positions. `None` if no winners.
    pub avg_win: Option<Decimal>,
    /// Average PnL of losing positions (negative). `None` if no losers.
    pub avg_loss: Option<Decimal>,
    /// Largest single winning PnL. `None` if no positions.
    pub max_win: Option<Decimal>,
    /// Most negative single PnL (largest loss). `None` if no positions.
    pub max_loss: Option<Decimal>,
    /// Average hold duration in seconds. `None` if no positions.
    pub avg_hold_duration_secs: Option<f64>,
}

/// Compute aggregate stats from the positions already stored for a run.
///
/// Must be called after all positions have been inserted.
pub async fn aggregate_stats(
    pool: &PgPool,
    run_id: Uuid,
) -> Result<PositionAggregateStats, DalError> {
    let row: (i64, i64, Decimal, Decimal, Option<Decimal>, Option<Decimal>, Option<Decimal>, Option<Decimal>, Option<f64>) = sqlx::query_as(
        "SELECT \
           COUNT(*) FILTER (WHERE pnl > 0), \
           COUNT(*) FILTER (WHERE pnl <= 0), \
           COALESCE(SUM(pnl), 0), \
           COALESCE(SUM(fees_enter + fees_exit), 0), \
           AVG(pnl) FILTER (WHERE pnl > 0), \
           AVG(pnl) FILTER (WHERE pnl <= 0), \
           MAX(pnl), \
           MIN(pnl), \
           AVG(EXTRACT(EPOCH FROM (time_exit - time_enter))) \
         FROM paper_run_positions \
         WHERE run_id = $1",
    )
    .bind(run_id)
    .fetch_one(pool)
    .await?;
    Ok(PositionAggregateStats {
        winning: row.0,
        losing: row.1,
        total_pnl_net: row.2,
        total_fees: row.3,
        avg_win: row.4,
        avg_loss: row.5,
        max_win: row.6,
        max_loss: row.7,
        avg_hold_duration_secs: row.8,
    })
}

/// Count all positions for a run.
pub async fn count_by_run(pool: &PgPool, run_id: Uuid) -> Result<i64, DalError> {
    let (count,): (i64,) =
        sqlx::query_as("SELECT COUNT(*) FROM paper_run_positions WHERE run_id = $1")
            .bind(run_id)
            .fetch_one(pool)
            .await?;
    Ok(count)
}

/// Fetch all positions for a run, ordered by seq.
pub async fn list_by_run(
    pool: &PgPool,
    run_id: Uuid,
) -> Result<Vec<PaperRunPositionRow>, DalError> {
    let rows = sqlx::query_as::<_, PaperRunPositionRow>(
        "SELECT id, run_id, seq, side, price_entry, price_exit, quantity, pnl, \
                fees_enter, fees_exit, time_enter, time_exit \
         FROM paper_run_positions \
         WHERE run_id = $1 \
         ORDER BY seq",
    )
    .bind(run_id)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}

/// Fetch a downsampled set of positions for a run using NTILE bucketing.
///
/// Returns at most `samples` rows — the last position in each NTILE bucket.
/// The final position is always included (it ends up in the last bucket).
/// If `samples` >= total row count the full set is returned unchanged.
pub async fn list_by_run_sampled(
    pool: &PgPool,
    run_id: Uuid,
    samples: i64,
) -> Result<Vec<PaperRunPositionRow>, DalError> {
    let rows = sqlx::query_as::<_, PaperRunPositionRow>(
        "WITH bucketed AS ( \
             SELECT *, NTILE($2::int) OVER (ORDER BY seq) AS bucket \
             FROM paper_run_positions \
             WHERE run_id = $1 \
         ), \
         bucket_last AS ( \
             SELECT MAX(seq) AS last_seq \
             FROM bucketed \
             GROUP BY bucket \
         ) \
         SELECT p.id, p.run_id, p.seq, p.side, p.price_entry, p.price_exit, \
                p.quantity, p.pnl, p.fees_enter, p.fees_exit, p.time_enter, p.time_exit \
         FROM paper_run_positions p \
         JOIN bucket_last bl ON p.seq = bl.last_seq \
         WHERE p.run_id = $1 \
         ORDER BY p.seq",
    )
    .bind(run_id)
    .bind(samples)
    .fetch_all(pool)
    .await?;

    Ok(rows)
}
