use crate::error::DalError;
use crate::models::paper_run::{PaperRunRow, PaperRunStatus};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use sqlx::PgPool;
use uuid::Uuid;

const COLS: &str = "id, status, mode, config, starts_at, finishes_at, risk_free_return,
                    result_summary, error_message, worker_id,
                    created_at, started_at, completed_at, candle_interval, strategy_id";

/// Create a new queued paper run.
#[allow(clippy::too_many_arguments)]
pub async fn create(
    pool: &PgPool,
    mode: &str,
    config: &serde_json::Value,
    starts_at: DateTime<Utc>,
    finishes_at: DateTime<Utc>,
    risk_free_return: Decimal,
    candle_interval: Option<&str>,
    strategy_id: Option<Uuid>,
) -> Result<PaperRunRow, DalError> {
    let sql = format!(
        "INSERT INTO paper_runs (mode, config, starts_at, finishes_at, risk_free_return, candle_interval, strategy_id)
         VALUES ($1, $2, $3, $4, $5, $6, $7)
         RETURNING {COLS}"
    );
    let row = sqlx::query_as::<_, PaperRunRow>(&sql)
        .bind(mode)
        .bind(config)
        .bind(starts_at)
        .bind(finishes_at)
        .bind(risk_free_return)
        .bind(candle_interval)
        .bind(strategy_id)
        .fetch_one(pool)
        .await?;

    Ok(row)
}

/// Claim the next queued run using FOR UPDATE SKIP LOCKED for safe concurrency.
/// Returns None if no queued runs are available.
pub async fn claim_next_queued(
    pool: &PgPool,
    worker_id: &str,
) -> Result<Option<PaperRunRow>, DalError> {
    let sql = format!(
        "UPDATE paper_runs
         SET status = 'running', worker_id = $1, started_at = now()
         WHERE id = (
             SELECT id FROM paper_runs
             WHERE status = 'queued' AND starts_at <= now()
             ORDER BY starts_at
             LIMIT 1
             FOR UPDATE SKIP LOCKED
         )
         RETURNING {COLS}"
    );
    let row = sqlx::query_as::<_, PaperRunRow>(&sql)
        .bind(worker_id)
        .fetch_optional(pool)
        .await?;

    Ok(row)
}

/// Mark a run as complete with a result summary.
pub async fn mark_complete(
    pool: &PgPool,
    id: Uuid,
    result_summary: &serde_json::Value,
) -> Result<PaperRunRow, DalError> {
    let sql = format!(
        "UPDATE paper_runs
         SET status = 'complete', result_summary = $2, completed_at = now()
         WHERE id = $1
         RETURNING {COLS}"
    );
    let row = sqlx::query_as::<_, PaperRunRow>(&sql)
        .bind(id)
        .bind(result_summary)
        .fetch_one(pool)
        .await?;

    Ok(row)
}

/// Mark a run as failed with an error message.
pub async fn mark_failed(
    pool: &PgPool,
    id: Uuid,
    error_message: &str,
) -> Result<PaperRunRow, DalError> {
    let sql = format!(
        "UPDATE paper_runs
         SET status = 'failed', error_message = $2, completed_at = now()
         WHERE id = $1
         RETURNING {COLS}"
    );
    let row = sqlx::query_as::<_, PaperRunRow>(&sql)
        .bind(id)
        .bind(error_message)
        .fetch_one(pool)
        .await?;

    Ok(row)
}

/// Cancel a queued run. Only queued runs can be cancelled.
pub async fn cancel(pool: &PgPool, id: Uuid) -> Result<Option<PaperRunRow>, DalError> {
    let sql = format!(
        "UPDATE paper_runs
         SET status = 'cancelled', completed_at = now()
         WHERE id = $1 AND status = 'queued'
         RETURNING {COLS}"
    );
    let row = sqlx::query_as::<_, PaperRunRow>(&sql)
        .bind(id)
        .fetch_optional(pool)
        .await?;

    Ok(row)
}

/// Get multiple runs by their ids. Ordering matches no particular guarantee; sort in the caller.
pub async fn get_by_ids(pool: &PgPool, ids: &[Uuid]) -> Result<Vec<PaperRunRow>, DalError> {
    let sql = format!("SELECT {COLS} FROM paper_runs WHERE id = ANY($1)");
    let rows = sqlx::query_as::<_, PaperRunRow>(&sql)
        .bind(ids)
        .fetch_all(pool)
        .await?;
    Ok(rows)
}

/// Get a run by id.
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<PaperRunRow>, DalError> {
    let sql = format!("SELECT {COLS} FROM paper_runs WHERE id = $1");
    let row = sqlx::query_as::<_, PaperRunRow>(&sql)
        .bind(id)
        .fetch_optional(pool)
        .await?;

    Ok(row)
}

/// List runs with optional status/strategy_id filters and pagination.
pub async fn list(
    pool: &PgPool,
    status: Option<PaperRunStatus>,
    strategy_id: Option<Uuid>,
    limit: i64,
    offset: i64,
) -> Result<Vec<PaperRunRow>, DalError> {
    let rows = match (status, strategy_id) {
        (None, None) => {
            let sql = format!(
                "SELECT {COLS} FROM paper_runs ORDER BY created_at DESC LIMIT $1 OFFSET $2"
            );
            sqlx::query_as::<_, PaperRunRow>(&sql)
                .bind(limit)
                .bind(offset)
                .fetch_all(pool)
                .await?
        }
        (Some(s), None) => {
            let sql = format!(
                "SELECT {COLS} FROM paper_runs
                 WHERE status = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3"
            );
            sqlx::query_as::<_, PaperRunRow>(&sql)
                .bind(s)
                .bind(limit)
                .bind(offset)
                .fetch_all(pool)
                .await?
        }
        (None, Some(sid)) => {
            let sql = format!(
                "SELECT {COLS} FROM paper_runs
                 WHERE strategy_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3"
            );
            sqlx::query_as::<_, PaperRunRow>(&sql)
                .bind(sid)
                .bind(limit)
                .bind(offset)
                .fetch_all(pool)
                .await?
        }
        (Some(s), Some(sid)) => {
            let sql = format!(
                "SELECT {COLS} FROM paper_runs
                 WHERE status = $1 AND strategy_id = $2
                 ORDER BY created_at DESC LIMIT $3 OFFSET $4"
            );
            sqlx::query_as::<_, PaperRunRow>(&sql)
                .bind(s)
                .bind(sid)
                .bind(limit)
                .bind(offset)
                .fetch_all(pool)
                .await?
        }
    };

    Ok(rows)
}

/// Reset stale running runs (started before `older_than`) back to queued.
pub async fn reset_stale_running(
    pool: &PgPool,
    older_than: DateTime<Utc>,
) -> Result<u64, DalError> {
    let result = sqlx::query(
        "UPDATE paper_runs
         SET status = 'queued', worker_id = NULL, started_at = NULL
         WHERE status = 'running' AND started_at < $1",
    )
    .bind(older_than)
    .execute(pool)
    .await?;

    Ok(result.rows_affected())
}
