use axum::{
    Json,
    extract::{Path, Query, State},
    http::StatusCode,
};
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::error::ApiError;
use crate::state::AppState;
use swym_dal::models::paper_run::{PaperRunRow, PaperRunStatus};
use swym_dal::models::paper_run_position::PaperRunPositionRow;
use swym_dal::models::strategy_config::{StrategyConfig, collect_timeframes};
use swym_dal::models::condition_audit::ConditionAuditRow;
use swym_dal::repo::{condition_audit, ingest_config, instrument, market_event, paper_run, paper_run_position, strategy};
use swym_dal::strategy_hash::{compute_strategy_hash, normalize_strategy};

// -- Request / Response types --

const VALID_CANDLE_INTERVALS: &[&str] = &["1m", "5m", "15m", "1h", "4h", "1d"];

#[derive(Debug, Deserialize)]
pub struct CreatePaperRunRequest {
    #[serde(default = "default_mode")]
    pub mode: String,
    pub config: serde_json::Value,
    pub starts_at: DateTime<Utc>,
    pub finishes_at: DateTime<Utc>,
    #[serde(default = "default_risk_free_return")]
    pub risk_free_return: Decimal,
    /// Candle interval for backtest mode. When set, replays candle data instead of raw trades.
    /// Valid values: '1m', '5m', '15m', '1h', '4h', '1d'. Only valid for mode='backtest'.
    pub candle_interval: Option<String>,
}

fn default_mode() -> String {
    "live".to_string()
}

fn default_risk_free_return() -> Decimal {
    Decimal::new(5, 2) // 0.05
}

#[derive(Debug, Deserialize)]
pub struct ListQuery {
    pub status: Option<PaperRunStatus>,
    pub strategy_id: Option<Uuid>,
    #[serde(default = "default_limit")]
    pub limit: i64,
    #[serde(default)]
    pub offset: i64,
}

fn default_limit() -> i64 {
    50
}

#[derive(Debug, Serialize)]
pub struct PaperRunResponse {
    pub id: Uuid,
    pub status: PaperRunStatus,
    pub mode: String,
    pub config: serde_json::Value,
    pub starts_at: DateTime<Utc>,
    pub finishes_at: DateTime<Utc>,
    pub risk_free_return: Decimal,
    pub result_summary: Option<serde_json::Value>,
    pub error_message: Option<String>,
    pub worker_id: Option<String>,
    pub created_at: DateTime<Utc>,
    pub started_at: Option<DateTime<Utc>>,
    pub completed_at: Option<DateTime<Utc>>,
    pub candle_interval: Option<String>,
    pub strategy_id: Option<Uuid>,
}

impl From<PaperRunRow> for PaperRunResponse {
    fn from(row: PaperRunRow) -> Self {
        Self {
            id: row.id,
            status: row.status,
            mode: row.mode,
            config: row.config,
            starts_at: row.starts_at,
            finishes_at: row.finishes_at,
            risk_free_return: row.risk_free_return,
            result_summary: row.result_summary,
            error_message: row.error_message,
            worker_id: row.worker_id,
            created_at: row.created_at,
            started_at: row.started_at,
            completed_at: row.completed_at,
            candle_interval: row.candle_interval,
            strategy_id: row.strategy_id,
        }
    }
}

// -- Handlers --

/// POST /api/v1/paper-runs
pub async fn create_paper_run(
    State(state): State<AppState>,
    Json(req): Json<CreatePaperRunRequest>,
) -> Result<(axum::http::StatusCode, Json<PaperRunResponse>), ApiError> {
    // Validate mode
    if req.mode != "live" && req.mode != "backtest" {
        return Err(ApiError::BadRequest(
            "mode must be 'live' or 'backtest'".to_owned(),
        ));
    }

    if req.finishes_at <= req.starts_at {
        return Err(ApiError::BadRequest(
            "finishes_at must be after starts_at".to_owned(),
        ));
    }

    // Validate entire config as a swym RunConfig (singular instrument/execution shape)
    let run_config: swym_dal::models::run_config::RunConfig =
        serde_json::from_value(req.config.clone())
            .map_err(|e| ApiError::BadRequest(format!("invalid run config: {e}")))?;

    // Additionally validate the instrument JSON against barter's InstrumentConfig
    serde_json::from_value::<barter::system::config::InstrumentConfig>(
        run_config.instrument.clone(),
    )
    .map_err(|e| ApiError::BadRequest(format!("invalid instrument config: {e}")))?;

    // For rule-based strategies the candle interval is embedded in the strategy config;
    // for all other strategies it comes from the top-level request field.
    let effective_candle_interval: Option<String> = match &run_config.strategy {
        StrategyConfig::RuleBased(params) => Some(params.candle_interval.clone()),
        _ => req.candle_interval.clone(),
    };

    match req.mode.as_str() {
        "live" => {
            if req.candle_interval.is_some() {
                return Err(ApiError::BadRequest(
                    "candle_interval is only valid for mode='backtest'".to_owned(),
                ));
            }
            if req.finishes_at <= Utc::now() {
                return Err(ApiError::BadRequest(
                    "finishes_at must be in the future for live runs".to_owned(),
                ));
            }
        }
        "backtest" => {
            if req.starts_at >= Utc::now() {
                return Err(ApiError::BadRequest(
                    "starts_at must be in the past for backtests".to_owned(),
                ));
            }

            // Validate candle_interval if provided (either from request or embedded in rule_based params).
            if let Some(ref interval) = effective_candle_interval {
                if !VALID_CANDLE_INTERVALS.contains(&interval.as_str()) {
                    return Err(ApiError::BadRequest(format!(
                        "invalid candle_interval '{}'; valid values: {}",
                        interval,
                        VALID_CANDLE_INTERVALS.join(", ")
                    )));
                }
            }

            // Extract exchange + instrument name from the instrument config JSON.
            let exchange_name = run_config
                .instrument
                .get("exchange")
                .and_then(|v| v.as_str())
                .ok_or_else(|| {
                    ApiError::BadRequest("instrument config missing 'exchange' field".to_owned())
                })?;
            let name_exchange = run_config
                .instrument
                .get("name_exchange")
                .and_then(|v| v.as_str())
                .ok_or_else(|| {
                    ApiError::BadRequest(
                        "instrument config missing 'name_exchange' field".to_owned(),
                    )
                })?;

            // Resolve instrument row.
            let instrument = swym_dal::repo::instrument::find_by_exchange_and_name(
                &state.pool,
                exchange_name,
                name_exchange,
            )
            .await?
            .ok_or_else(|| {
                ApiError::BadRequest(format!(
                    "instrument {name_exchange} on {exchange_name} not found in database"
                ))
            })?;

            if let Some(ref interval) = effective_candle_interval {
                // Candle mode: verify candle data is available for the range.
                let range = swym_dal::repo::market_event::candle_data_range(
                    &state.pool,
                    instrument.id,
                    interval,
                )
                .await?
                .ok_or_else(|| {
                    ApiError::BadRequest(format!(
                        "no {interval} candle data available for {name_exchange} on {exchange_name}; \
                         run a backfill first via POST /api/v1/market-candles/backfill"
                    ))
                })?;

                if req.starts_at < range.0 || req.finishes_at > range.1 {
                    return Err(ApiError::BadRequest(format!(
                        "requested range {starts} – {ends} is outside available {interval} candle data \
                         {data_start} – {data_end}",
                        starts = req.starts_at,
                        ends = req.finishes_at,
                        data_start = range.0,
                        data_end = range.1,
                    )));
                }

                validate_candle_completeness(
                    &state.pool,
                    instrument.id,
                    &format!("{name_exchange} on {exchange_name}"),
                    interval,
                    req.starts_at,
                    req.finishes_at,
                )
                .await?;

                // For rule-based strategies, also validate every additional timeframe
                // referenced by expressions in the rule tree.
                if let StrategyConfig::RuleBased(ref params) = run_config.strategy {
                    let all_timeframes = collect_timeframes(params);
                    let mut additional: Vec<&str> = all_timeframes
                        .iter()
                        .map(String::as_str)
                        .filter(|&tf| tf != interval.as_str())
                        .collect();
                    additional.sort_unstable();

                    for tf in additional {
                        if !VALID_CANDLE_INTERVALS.contains(&tf) {
                            return Err(ApiError::BadRequest(format!(
                                "strategy references unknown timeframe '{tf}'; valid values: {}",
                                VALID_CANDLE_INTERVALS.join(", ")
                            )));
                        }

                        let tf_range = swym_dal::repo::market_event::candle_data_range(
                            &state.pool,
                            instrument.id,
                            tf,
                        )
                        .await?
                        .ok_or_else(|| {
                            ApiError::BadRequest(format!(
                                "strategy references timeframe '{tf}' but no {tf} candle data is available \
                                 for {name_exchange} on {exchange_name}; \
                                 run a backfill first via POST /api/v1/market-candles/backfill"
                            ))
                        })?;

                        if req.starts_at < tf_range.0 || req.finishes_at > tf_range.1 {
                            return Err(ApiError::BadRequest(format!(
                                "requested range {starts} – {ends} is outside available {tf} candle data \
                                 {data_start} – {data_end}",
                                starts = req.starts_at,
                                ends = req.finishes_at,
                                data_start = tf_range.0,
                                data_end = tf_range.1,
                            )));
                        }

                        validate_candle_completeness(
                            &state.pool,
                            instrument.id,
                            &format!("{name_exchange} on {exchange_name}"),
                            tf,
                            req.starts_at,
                            req.finishes_at,
                        )
                        .await?;
                    }
                }
            } else {
                // Tick replay mode: verify trade data is available.
                let range = swym_dal::repo::market_event::trade_data_range(&state.pool, instrument.id)
                    .await?
                    .ok_or_else(|| {
                        ApiError::BadRequest(format!(
                            "no trade data available for {name_exchange} on {exchange_name}"
                        ))
                    })?;

                if req.starts_at < range.0 || req.finishes_at > range.1 {
                    return Err(ApiError::BadRequest(format!(
                        "requested range {starts} – {ends} is outside available data {data_start} – {data_end}",
                        starts = req.starts_at,
                        ends = req.finishes_at,
                        data_start = range.0,
                        data_end = range.1,
                    )));
                }

                // Reject extremely large ranges to prevent OOM (~100 bytes/trade).
                let count = swym_dal::repo::market_event::count_trades(
                    &state.pool,
                    instrument.id,
                    req.starts_at,
                    req.finishes_at,
                )
                .await?;

                const MAX_TRADES: i64 = 500_000_000;
                if count > MAX_TRADES {
                    return Err(ApiError::BadRequest(format!(
                        "date range contains {count} trades; reduce the range (max {MAX_TRADES})"
                    )));
                }
            }
        }
        _ => unreachable!(),
    }

    // Upsert the strategy and link it to the run.
    // The strategy JSON lives at config["strategy"]; we re-serialize from the parsed
    // StrategyConfig to get a canonical form before normalizing.
    let strategy_json = serde_json::to_value(&run_config.strategy)
        .map_err(|e| ApiError::BadRequest(format!("failed to serialize strategy: {e}")))?;
    let hash_input = normalize_strategy(&strategy_json);
    let hash = compute_strategy_hash(&hash_input);
    let strategy_row = strategy::upsert(&state.pool, &hash, &strategy_json, &hash_input).await?;

    let row = paper_run::create(
        &state.pool,
        &req.mode,
        &req.config,
        req.starts_at,
        req.finishes_at,
        req.risk_free_return,
        effective_candle_interval.as_deref(),
        Some(strategy_row.id),
    )
    .await?;

    Ok((axum::http::StatusCode::CREATED, Json(row.into())))
}

/// GET /api/v1/paper-runs
pub async fn list_paper_runs(
    State(state): State<AppState>,
    Query(query): Query<ListQuery>,
) -> Result<Json<Vec<PaperRunResponse>>, ApiError> {
    let rows = paper_run::list(&state.pool, query.status, query.strategy_id, query.limit, query.offset).await?;
    let responses: Vec<PaperRunResponse> = rows.into_iter().map(Into::into).collect();
    Ok(Json(responses))
}

/// GET /api/v1/paper-runs/{id}
pub async fn get_paper_run(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<PaperRunResponse>, ApiError> {
    let row = paper_run::get_by_id(&state.pool, id)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("paper run {id} not found")))?;

    Ok(Json(row.into()))
}

/// GET /api/v1/instruments/{exchange}/{name}/data-range
pub async fn get_instrument_data_range(
    State(state): State<AppState>,
    Path((exchange, name)): Path<(String, String)>,
) -> Result<(StatusCode, Json<serde_json::Value>), ApiError> {
    let instrument = swym_dal::repo::instrument::find_by_exchange_and_name(
        &state.pool,
        &exchange,
        &name,
    )
    .await?;

    let Some(instrument) = instrument else {
        return Ok((StatusCode::NOT_FOUND, Json(serde_json::Value::Null)));
    };

    let range = swym_dal::repo::market_event::trade_data_range(&state.pool, instrument.id).await?;

    match range {
        Some((start, end)) => Ok((
            StatusCode::OK,
            Json(serde_json::json!({ "start": start, "end": end })),
        )),
        None => Ok((StatusCode::OK, Json(serde_json::Value::Null))),
    }
}

/// GET /api/v1/paper-runs/{id}/positions
pub async fn list_paper_run_positions(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Query(query): Query<PositionsQuery>,
) -> Result<Json<PositionsResponse>, ApiError> {
    let total = paper_run_position::count_by_run(&state.pool, id).await?;
    let positions = match query.samples {
        Some(n) if n > 0 && n < total => {
            paper_run_position::list_by_run_sampled(&state.pool, id, n).await?
        }
        _ => paper_run_position::list_by_run(&state.pool, id).await?,
    };
    let sampled = positions.len() as i64;
    Ok(Json(PositionsResponse { total, sampled, positions }))
}

#[derive(Debug, Deserialize)]
pub struct PositionsQuery {
    /// Maximum number of data points to return (uniform downsampling via NTILE).
    pub samples: Option<i64>,
}

#[derive(Debug, Serialize)]
pub struct PositionsResponse {
    /// True total number of closed positions for this run.
    pub total: i64,
    /// Number of positions returned (may be less than total when downsampled).
    pub sampled: i64,
    pub positions: Vec<PaperRunPositionRow>,
}

/// POST /api/v1/paper-runs/{id}/cancel
pub async fn cancel_paper_run(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<PaperRunResponse>, ApiError> {
    let row = paper_run::cancel(&state.pool, id)
        .await?
        .ok_or_else(|| {
            ApiError::BadRequest(format!(
                "paper run {id} not found or not in queued status"
            ))
        })?;

    Ok(Json(row.into()))
}

// ---------------------------------------------------------------------------
// Condition audit endpoints
// ---------------------------------------------------------------------------

/// GET /api/v1/paper-runs/{id}/condition-audit
///
/// Returns the condition pass-rate summary computed during the backtest.
/// The summary is stored in `result_summary.condition_audit_summary` and is
/// available for all rule-based backtests (not just ones with `audit_conditions: true`).
/// Returns 404 if no summary is present (non-rule-based run or incomplete run).
pub async fn get_condition_audit_summary(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<serde_json::Value>, ApiError> {
    let row = paper_run::get_by_id(&state.pool, id)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("paper run {id} not found")))?;

    let summary = row
        .result_summary
        .as_ref()
        .and_then(|s| s.get("condition_audit_summary"))
        .and_then(|s| if s.is_null() { None } else { Some(s.clone()) })
        .ok_or_else(|| {
            ApiError::NotFound(
                "no condition audit summary for this run (run must be a completed rule-based backtest)".to_string()
            )
        })?;

    Ok(Json(summary))
}

#[derive(Debug, Deserialize)]
pub struct ConditionAuditDetailQuery {
    #[serde(default)]
    pub offset: i64,
    #[serde(default = "default_audit_limit")]
    pub limit: i64,
}

fn default_audit_limit() -> i64 {
    1000
}

#[derive(Debug, Serialize)]
pub struct ConditionAuditDetailResponse {
    pub total: i64,
    pub offset: i64,
    pub limit: i64,
    pub rows: Vec<ConditionAuditRow>,
}

/// GET /api/v1/paper-runs/{id}/condition-audit/detail
///
/// Returns paginated per-bar per-rule audit rows. Only available when the run
/// was created with `audit_conditions: true` in the config.
/// Returns 404 if no detail data exists.
pub async fn get_condition_audit_detail(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
    Query(query): Query<ConditionAuditDetailQuery>,
) -> Result<Json<ConditionAuditDetailResponse>, ApiError> {
    let total = condition_audit::count_by_run(&state.pool, id).await?;
    if total == 0 {
        return Err(ApiError::NotFound(
            "no detailed condition audit data for this run — re-run with audit_conditions: true in config".to_string()
        ));
    }
    let limit = query.limit.clamp(1, 10_000);
    let rows = condition_audit::list_detail_page(&state.pool, id, query.offset, limit).await?;
    Ok(Json(ConditionAuditDetailResponse {
        total,
        offset: query.offset,
        limit,
        rows,
    }))
}

/// GET /api/v1/paper-runs/{id}/candles
#[derive(Debug, Serialize)]
pub struct CandlesResponse {
    pub interval: String,
    pub total: usize,
    pub candles: Vec<CandleEntry>,
}

#[derive(Debug, Serialize)]
pub struct CandleEntry {
    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 async fn list_paper_run_candles(
    State(state): State<AppState>,
    Path(id): Path<Uuid>,
) -> Result<Json<CandlesResponse>, ApiError> {
    let run = paper_run::get_by_id(&state.pool, id)
        .await?
        .ok_or_else(|| ApiError::NotFound(format!("paper run {id} not found")))?;

    let interval = run.candle_interval.as_ref().ok_or_else(|| {
        ApiError::BadRequest("this run uses tick data (no candle_interval set)".into())
    })?;

    let run_config: swym_dal::models::run_config::RunConfig =
        serde_json::from_value(run.config.clone())
            .map_err(|e| ApiError::BadRequest(format!("invalid run config: {e}")))?;

    let exchange_name = run_config
        .instrument
        .get("exchange")
        .and_then(|v| v.as_str())
        .ok_or_else(|| ApiError::BadRequest("config missing instrument.exchange".into()))?;
    let name_exchange = run_config
        .instrument
        .get("name_exchange")
        .and_then(|v| v.as_str())
        .ok_or_else(|| ApiError::BadRequest("config missing instrument.name_exchange".into()))?;

    let instrument_row = instrument::find_by_exchange_and_name(&state.pool, exchange_name, name_exchange)
        .await?
        .ok_or_else(|| {
            ApiError::NotFound(format!("instrument {name_exchange} on {exchange_name} not found"))
        })?;

    let rows = market_event::load_candles_range(
        &state.pool,
        instrument_row.id,
        interval,
        run.starts_at,
        run.finishes_at,
    )
    .await?;

    let candles: Vec<CandleEntry> = rows
        .into_iter()
        .map(|r| CandleEntry {
            open_time: r.open_time,
            close_time: r.close_time,
            open: r.open,
            high: r.high,
            low: r.low,
            close: r.close,
            volume: r.volume,
        })
        .collect();

    let total = candles.len();
    Ok(Json(CandlesResponse {
        interval: interval.clone(),
        total,
        candles,
    }))
}

// ---------------------------------------------------------------------------
// Compare endpoint
// ---------------------------------------------------------------------------

#[derive(Debug, Deserialize)]
pub struct CompareQuery {
    /// Comma-separated list of run UUIDs to compare.
    pub ids: String,
}

/// Flat metrics extracted from a run's `result_summary` for easy comparison.
#[derive(Debug, Serialize)]
pub struct RunMetricsSummary {
    pub id: Uuid,
    pub status: PaperRunStatus,
    pub strategy_id: Option<Uuid>,
    pub starts_at: DateTime<Utc>,
    pub finishes_at: DateTime<Utc>,
    pub candle_interval: Option<String>,
    pub total_positions: Option<i64>,
    pub win_rate: Option<Decimal>,
    pub profit_factor: Option<Decimal>,
    pub net_pnl: Option<Decimal>,
    pub total_pnl: Option<Decimal>,
    pub total_fees: Option<Decimal>,
    pub sharpe_ratio: Option<Decimal>,
    pub sortino_ratio: Option<Decimal>,
    pub calmar_ratio: Option<Decimal>,
    pub max_drawdown: Option<Decimal>,
    pub pnl_return: Option<Decimal>,
    pub avg_win: Option<Decimal>,
    pub avg_loss: Option<Decimal>,
    pub max_win: Option<Decimal>,
    pub max_loss: Option<Decimal>,
    pub avg_hold_duration_secs: Option<f64>,
}

fn extract_decimal(v: &serde_json::Value, key: &str) -> Option<Decimal> {
    v.get(key).and_then(|val| {
        val.as_str()
            .and_then(|s| s.parse().ok())
            .or_else(|| val.as_f64().and_then(|f| Decimal::try_from(f).ok()))
    })
}

fn row_to_metrics(row: PaperRunRow) -> RunMetricsSummary {
    let rs = row.result_summary.as_ref();
    RunMetricsSummary {
        id: row.id,
        status: row.status,
        strategy_id: row.strategy_id,
        starts_at: row.starts_at,
        finishes_at: row.finishes_at,
        candle_interval: row.candle_interval,
        total_positions: rs.and_then(|v| v.get("total_positions")).and_then(|v| v.as_i64()),
        win_rate: rs.and_then(|v| extract_decimal(v, "win_rate")),
        profit_factor: rs.and_then(|v| extract_decimal(v, "profit_factor")),
        net_pnl: rs.and_then(|v| extract_decimal(v, "net_pnl")),
        total_pnl: rs.and_then(|v| extract_decimal(v, "total_pnl")),
        total_fees: rs.and_then(|v| extract_decimal(v, "total_fees")),
        sharpe_ratio: rs.and_then(|v| extract_decimal(v, "sharpe_ratio")),
        sortino_ratio: rs.and_then(|v| extract_decimal(v, "sortino_ratio")),
        calmar_ratio: rs.and_then(|v| extract_decimal(v, "calmar_ratio")),
        max_drawdown: rs.and_then(|v| extract_decimal(v, "max_drawdown")),
        pnl_return: rs.and_then(|v| extract_decimal(v, "pnl_return")),
        avg_win: rs.and_then(|v| extract_decimal(v, "avg_win")),
        avg_loss: rs.and_then(|v| extract_decimal(v, "avg_loss")),
        max_win: rs.and_then(|v| extract_decimal(v, "max_win")),
        max_loss: rs.and_then(|v| extract_decimal(v, "max_loss")),
        avg_hold_duration_secs: rs.and_then(|v| v.get("avg_hold_duration_secs")).and_then(|v| v.as_f64()),
    }
}

/// GET /api/v1/paper-runs/compare?ids=<uuid1>,<uuid2>,...
///
/// Returns flat metrics for multiple runs in a single response. Useful for
/// side-by-side strategy comparison without fetching full result_summary blobs.
pub async fn compare_paper_runs(
    State(state): State<AppState>,
    Query(query): Query<CompareQuery>,
) -> Result<Json<Vec<RunMetricsSummary>>, ApiError> {
    let ids: Vec<Uuid> = query
        .ids
        .split(',')
        .map(|s| s.trim().parse::<Uuid>())
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| ApiError::BadRequest(format!("invalid UUID in ids: {e}")))?;

    if ids.is_empty() {
        return Err(ApiError::BadRequest("ids must not be empty".to_owned()));
    }
    if ids.len() > 50 {
        return Err(ApiError::BadRequest(
            "too many ids; maximum is 50".to_owned(),
        ));
    }

    let rows = paper_run::get_by_ids(&state.pool, &ids).await?;
    // Return results in the same order as the requested ids.
    let mut map: std::collections::HashMap<Uuid, RunMetricsSummary> =
        rows.into_iter().map(|r| (r.id, row_to_metrics(r))).collect();
    let result: Vec<RunMetricsSummary> = ids.into_iter().filter_map(|id| map.remove(&id)).collect();

    Ok(Json(result))
}

// ---------------------------------------------------------------------------
// Candle completeness validation
// ---------------------------------------------------------------------------

const MIN_CANDLE_COVERAGE: f64 = 0.95;

/// Validate that candle coverage for `[from, to)` meets the minimum threshold.
///
/// Computes the expected candle count from the interval duration and compares
/// it to the actual count in the database. Returns `Err(BadRequest)` with a
/// diagnostic message (including an ingestion status hint) when coverage is
/// below [`MIN_CANDLE_COVERAGE`].
async fn validate_candle_completeness(
    pool: &sqlx::PgPool,
    instrument_id: i32,
    instrument_label: &str,
    interval: &str,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
) -> Result<(), ApiError> {
    use swym_dal::models::strategy_config::parse_interval_secs;

    let interval_secs = parse_interval_secs(interval)
        .expect("interval already validated before this call");
    let range_secs = (to - from).num_seconds().max(0) as u64;
    let expected = (range_secs / interval_secs) as i64;

    if expected == 0 {
        return Ok(());
    }

    let actual = market_event::count_candles(pool, instrument_id, interval, from, to).await?;
    let coverage = actual as f64 / expected as f64;

    if coverage >= MIN_CANDLE_COVERAGE {
        return Ok(());
    }

    // Build an ingestion status hint from the candle cursor.
    let cursor = ingest_config::get_candle_cursor(pool, instrument_id, interval).await?;
    let ingestion_hint = match cursor {
        Some(date) => {
            let yesterday = (Utc::now() - chrono::Duration::days(1)).date_naive();
            if date >= yesterday {
                "Candle ingestion appears up to date; the data may be genuinely sparse \
                 for this period."
                    .to_string()
            } else {
                format!(
                    "Candle ingestion last reached {date}; it may still be catching up. \
                     Retry later or trigger a backfill via POST /api/v1/market-candles/backfill."
                )
            }
        }
        None => {
            "No candle ingestion cursor found for this interval. \
             Trigger a backfill via POST /api/v1/market-candles/backfill."
                .to_string()
        }
    };

    Err(ApiError::BadRequest(format!(
        "insufficient {interval} candle data for {instrument_label}: \
         {actual} of {expected} expected candles available \
         ({pct:.1}% coverage, minimum {min:.0}%). {ingestion_hint}",
        pct = coverage * 100.0,
        min = MIN_CANDLE_COVERAGE * 100.0,
    )))
}
