use crate::run_config::{RunConfig, into_system_config};
use crate::strategy::{instrument_data::SwymInstrumentData, swym_strategy::SwymStrategy};
use barter_integration::Terminal;
use barter::{
    engine::{
        clock::LiveClock,
        state::{
            global::DefaultGlobalData,
            instrument::filter::InstrumentFilter,
            trading::TradingState,
        },
    },
    risk::DefaultRiskManager,
    statistic::time::Daily,
    system::builder::{AuditMode, EngineFeedMode, SystemArgs, SystemBuilder},
};
use barter_data::{
    books::Level,
    event::{DataKind, MarketEvent},
    streams::{
        builder::dynamic::indexed::init_indexed_multi_exchange_market_stream,
        consumer::MarketStreamEvent,
    },
    subscription::{SubKind, book::OrderBookL1, candle::Candle, trade::PublicTrade},
};
use barter_execution::balance::Balance as BarterBalance;
use barter_instrument::{
    Keyed,
    Side,
    asset::{ExchangeAsset, name::AssetNameInternal},
    exchange::ExchangeId,
    instrument::InstrumentIndex,
};
use chrono::{DateTime, Utc};
use futures::StreamExt;
use rust_decimal::Decimal;
use sqlx::PgPool;
use swym_backtest::BacktestEngine;
use swym_dal::repo::paper_run_position::{self, InsertPosition};
use swym_dal::repo::condition_audit::{self, InsertAuditRow};
use crate::strategy::audit::{BarAudit, compute_summary, new_audit_log};
use tracing::{info, debug, warn};
use uuid::Uuid;

/// Execute a live paper trading run and return the trading summary as JSON.
pub async fn execute_run(
    config: RunConfig,
    _starts_at: DateTime<Utc>,
    finishes_at: DateTime<Utc>,
    risk_free_return: Decimal,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
    let (system, strategy_config) = into_system_config(config)?;

    let instruments = barter_instrument::index::IndexedInstruments::new(system.instruments);

    let market_stream = init_indexed_multi_exchange_market_stream(
        &instruments,
        &[SubKind::PublicTrades, SubKind::OrderBooksL1],
    )
    .await?;

    let strategy = SwymStrategy::from_config(&strategy_config);
    let ema_regs = strategy.ema_registrations();
    let candle_interval_secs = strategy.candle_interval_secs();
    let additional_secs: Vec<u64> = strategy.all_interval_secs()
        .into_iter()
        .filter(|&s| Some(s) != candle_interval_secs)
        .collect();

    let system_args = SystemArgs::new(
        &instruments,
        system.executions,
        LiveClock,
        strategy,
        DefaultRiskManager::default(),
        market_stream,
        DefaultGlobalData::default(),
        move |_| {
            let mut data = if let Some(primary) = candle_interval_secs {
                SwymInstrumentData::with_multi_timeframes(primary, &additional_secs)
            } else {
                SwymInstrumentData::default()
            };
            for &(secs, period) in &ema_regs {
                data.register_ema(secs, period);
            }
            data
        },
    );

    let mut system = SystemBuilder::new(system_args)
        .engine_feed_mode(EngineFeedMode::Iterator)
        .audit_mode(AuditMode::Enabled)
        .trading_state(TradingState::Disabled)
        .build()?
        .init_with_runtime(tokio::runtime::Handle::current())
        .await?;

    let audit = system.audit.take().unwrap();

    let audit_task = tokio::spawn(async move {
        let mut audit_stream = audit.updates.into_stream();
        while let Some(audit) = audit_stream.next().await {
            debug!(?audit, "AuditStream consumed AuditTick");
            if audit.event.is_terminal() {
                break;
            }
        }
        audit_stream
    });

    system.trading_state(TradingState::Enabled);

    // Run until finishes_at
    let now = Utc::now();
    if finishes_at <= now {
        return Err("finish time has already passed".into());
    }
    let run_duration = (finishes_at - now).to_std()?;
    info!(?finishes_at, secs = run_duration.as_secs(), "paper trade session running");
    tokio::time::sleep(run_duration).await;

    system.cancel_orders(InstrumentFilter::None);
    system.close_positions(InstrumentFilter::None);

    let (engine, _shutdown_audit) = system.shutdown().await?;
    let _audit_stream = audit_task.await?;

    let trading_summary = engine
        .trading_summary_generator(risk_free_return)
        .generate(Daily);

    // TradingSummary.assets is keyed by ExchangeAsset<AssetNameInternal>, which is a struct
    // containing an enum (ExchangeId). serde_json cannot serialize non-string map keys, so we
    // convert assets to an array of objects with explicit exchange/asset/tear_sheet fields.
    let assets: Vec<serde_json::Value> = trading_summary
        .assets
        .iter()
        .map(|(k, v)| {
            serde_json::json!({
                "exchange": k.exchange,
                "asset": k.asset,
                "tear_sheet": v,
            })
        })
        .collect();

    let tear_sheet = trading_summary.instruments.values().next();
    let win_rate = tear_sheet.and_then(|t| t.win_rate.as_ref()).map(|w| w.value);
    let profit_factor = tear_sheet.and_then(|t| t.profit_factor.as_ref()).map(|p| p.value);
    let sharpe_ratio = tear_sheet.map(|t| t.sharpe_ratio.value);
    let sortino_ratio = tear_sheet.map(|t| t.sortino_ratio.value);
    let calmar_ratio = tear_sheet.map(|t| t.calmar_ratio.value);
    let max_drawdown = tear_sheet.and_then(|t| t.pnl_drawdown_max.as_ref()).map(|d| d.0.value);
    let pnl_return = tear_sheet.map(|t| t.pnl_return.value);
    let net_pnl = tear_sheet.map(|t| t.pnl);

    let result = serde_json::json!({
        "win_rate": win_rate,
        "profit_factor": profit_factor,
        "net_pnl": net_pnl,
        "sharpe_ratio": sharpe_ratio,
        "sortino_ratio": sortino_ratio,
        "calmar_ratio": calmar_ratio,
        "max_drawdown": max_drawdown,
        "pnl_return": pnl_return,
        "time_engine_start": trading_summary.time_engine_start,
        "time_engine_end": trading_summary.time_engine_end,
        "instruments": trading_summary.instruments,
        "assets": assets,
    });

    Ok(result)
}

/// Execute a historical backtest by replaying market data from the database.
///
/// When `candle_interval` is `Some`, loads OHLCV candle data from `market_candles`
/// and feeds `DataKind::Candle` events to the engine. This is ~500x faster than
/// tick replay for indicator-based strategies (fewer rows, fewer indicator evaluations).
///
/// When `candle_interval` is `None`, uses the existing tick-replay path.
pub async fn execute_backtest(
    pool: &PgPool,
    run_id: Uuid,
    config: RunConfig,
    starts_at: DateTime<Utc>,
    finishes_at: DateTime<Utc>,
    risk_free_return: Decimal,
    candle_interval: Option<&str>,
) -> Result<serde_json::Value, Box<dyn std::error::Error + Send + Sync>> {
    // Force zero latency for backtests: the mock exchange sleeps latency_ms before sending fills,
    // and the engine shuts down immediately after the market stream ends. Any non-zero latency
    // causes fills to arrive after Shutdown, so balances never update.
    let mut config = config;
    config.execution.latency_ms = 0;

    let (system, strategy_config) = into_system_config(config.clone())?;

    // Extract exchange name and instrument name from the config's instrument JSON.
    let (exchange_name, name_exchange) = extract_instrument_identifiers(&config.instrument)?;

    // Resolve instrument_id from the DB.
    let instrument_row =
        swym_dal::repo::instrument::find_by_exchange_and_name(pool, &exchange_name, &name_exchange)
            .await?
            .ok_or_else(|| {
                format!("instrument {name_exchange} on {exchange_name} not found in database")
            })?;

    // Parse the exchange ID for event construction.
    let exchange_id: ExchangeId =
        serde_json::from_value(serde_json::Value::String(exchange_name))?;

    // For a single-instrument run the instrument index is always 0.
    let instrument_index = InstrumentIndex(0);

    let instruments = barter_instrument::index::IndexedInstruments::new(system.instruments);

    // Map RunConfig balances → Keyed<ExchangeAsset<AssetNameInternal>, Balance>
    let balance_exchange_id: ExchangeId = serde_json::from_value(
        serde_json::Value::String(config.execution.initial_state.exchange.clone()),
    )?;
    let balances: Vec<Keyed<ExchangeAsset<AssetNameInternal>, BarterBalance>> = config
        .execution
        .initial_state
        .balances
        .iter()
        .map(|b| Keyed {
            key: ExchangeAsset {
                exchange: balance_exchange_id,
                asset: AssetNameInternal::from(b.asset.as_str()),
            },
            value: BarterBalance {
                total: b.balance.total,
                free: b.balance.free,
            },
        })
        .collect();

    let mut strategy = SwymStrategy::from_config(&strategy_config);

    // Always enable condition auditing for rule-based backtests so that the
    // pass-rate summary is available in result_summary regardless of the
    // `audit_conditions` flag. (When the flag is false we still collect BarAudit
    // entries but do not persist them to the DB — they are only used for the summary.)
    let audit_log = if matches!(strategy_config, swym_dal::models::strategy_config::StrategyConfig::RuleBased(_)) {
        let log = new_audit_log();
        strategy.set_audit_log(log.clone());
        Some(log)
    } else {
        None
    };

    // All BarAudit entries drained across all chunks; used to compute the final summary.
    let mut all_audits: Vec<BarAudit> = Vec::new();

    const PAGE_SIZE: i64 = 100_000;

    if let Some(interval) = candle_interval {
        // --- Candle-based backtest path ---

        // Compute the indicator warmup period needed by this strategy. Long-period indicators
        // (e.g. SMA(200) on 4h = ~33 days) require candle history accumulated before `starts_at`
        // or their first N evaluations will report insufficient data. Pre-loading warmup candles
        // eliminates the dead zone at the start of the backtest window.
        let warmup_secs =
            swym_dal::models::strategy_config::compute_warmup_secs(&strategy_config);
        let warmup_duration = chrono::Duration::seconds(warmup_secs as i64);
        let data_starts_at = starts_at - warmup_duration;

        if warmup_secs > 0 {
            info!(%run_id, warmup_secs, %data_starts_at, "pre-loading indicator warmup candles");
        }

        let first_candle_page = swym_dal::repo::market_event::load_candles_page(
            pool,
            instrument_row.id,
            interval,
            starts_at,
            finishes_at,
            0,
            PAGE_SIZE,
        )
        .await?;

        if first_candle_page.is_empty() {
            return Err(format!(
                "no {interval} candle data found for {name_exchange} between {starts_at} and {finishes_at}"
            )
            .into());
        }

        let first_event_time = first_candle_page[0].open_time;
        let primary_secs = strategy.candle_interval_secs().unwrap_or(0);
        let ema_regs = strategy.ema_registrations();
        let additional_intervals: Vec<&'static str> = strategy.additional_intervals();
        let additional_secs_list: Vec<u64> = strategy.all_interval_secs()
            .into_iter()
            .filter(|&s| s != primary_secs)
            .collect();

        // Load all additional-interval candles upfront (they are infrequent: ≤8760 per year
        // for 1h, ≤365 for 1d). Load from `data_starts_at` (= starts_at - warmup) so the
        // full warmup history is available before engine start.
        let mut add_candles: Vec<(u64, swym_dal::models::market_event::MarketCandleRow)> = Vec::new();
        for (&add_secs, add_interval) in additional_secs_list.iter().zip(additional_intervals.iter()) {
            let rows = load_all_candles(
                pool, instrument_row.id, add_interval, data_starts_at, finishes_at,
            ).await?;
            if rows.is_empty() {
                warn!(%run_id, add_interval, "no candle data for additional timeframe — expressions referencing it will return None");
            }
            for row in rows {
                add_candles.push((add_secs, row));
            }
        }
        add_candles.sort_by_key(|(_, row)| row.close_time);

        info!(interval, additional = ?additional_intervals, "beginning chunked candle backtest replay");

        // Initialise shared per-instrument data. This is passed to new_with_init below so the
        // engine starts with pre-warmed indicator histories built from the warmup candles above.
        let mut warmup_instrument_data = SwymInstrumentData::default();
        warmup_instrument_data.init_timeframes(primary_secs, &additional_secs_list);
        for &(secs, period) in &ema_regs {
            warmup_instrument_data.register_ema(secs, period);
        }

        // Pre-process warmup candles (those before starts_at) through instrument data only.
        // This populates candle histories and EMA states without generating any orders.
        // We process all additional-interval warmup candles first, then primary warmup candles,
        // interleaved in time order.
        if warmup_secs > 0 {
            // Collect primary warmup candles.
            let primary_warmup = load_all_candles(
                pool, instrument_row.id, interval, data_starts_at, starts_at,
            ).await?;

            // Collect additional warmup candles (those with close_time < starts_at).
            let add_warmup_end = add_candles.partition_point(|(_, row)| row.close_time < starts_at);

            // Build a merged time-sorted sequence for warmup.
            let mut warmup_tagged: Vec<(u64, swym_dal::models::market_event::MarketCandleRow)> =
                Vec::with_capacity(primary_warmup.len() + add_warmup_end);
            for row in primary_warmup {
                warmup_tagged.push((primary_secs, row));
            }
            for (add_secs, row) in &add_candles[..add_warmup_end] {
                warmup_tagged.push((*add_secs, row.clone()));
            }
            warmup_tagged.sort_by_key(|(_, row)| row.close_time);

            for (hint_secs, row) in warmup_tagged {
                let close = Decimal::from_f64_retain(row.close).unwrap_or_default();
                let open  = Decimal::from_f64_retain(row.open).unwrap_or(close);
                let high  = Decimal::from_f64_retain(row.high).unwrap_or(close);
                let low   = Decimal::from_f64_retain(row.low).unwrap_or(close);
                let vol   = Decimal::from_f64_retain(row.volume).unwrap_or_default();
                warmup_instrument_data.process_candle_warmup(
                    hint_secs, row.open_time, open, high, low, close, vol,
                );
            }

            // Remove the warmup slice from add_candles so the main loop only sees candles >= starts_at.
            add_candles.drain(..add_warmup_end);

            info!(%run_id, warmup_secs, "indicator warmup complete");
        }

        let warmed_data = warmup_instrument_data;

        let mut engine = BacktestEngine::<SwymInstrumentData>::new_with_init(
            instruments,
            balances,
            config.execution.fees_percent,
            risk_free_return,
            first_event_time,
            move |_| warmed_data.clone(),
        );

        let mut run_state = engine.begin_run();

        let mut last_id: i64 = 0;
        let mut total_candle_count: usize = 0;
        let mut total_position_count: usize = 0;
        let mut data_start: Option<DateTime<Utc>> = None;
        let mut data_end: Option<DateTime<Utc>> = None;
        let mut page = first_candle_page;
        // Cursor into `add_candles` — advances as we interleave additional-interval candles.
        let mut add_cursor: usize = 0;

        loop {
            if page.is_empty() {
                break;
            }

            if data_start.is_none() {
                data_start = Some(page[0].open_time);
            }
            data_end = page.last().map(|c| c.close_time);
            last_id = page.last().map(|c| c.id).unwrap_or(last_id);
            total_candle_count += page.len();
            let fetched = page.len();

            // Build a merged, time-sorted sequence of (interval_hint, event) pairs.
            // Additional-interval candles that close at or before this chunk's last close_time
            // are included. Primary candles carry their hint; secondary carry theirs.
            let chunk_end = page.last().map(|c| c.close_time).unwrap_or(finishes_at);

            // Collect additional-interval events up to chunk_end.
            let add_start = add_cursor;
            while add_cursor < add_candles.len() && add_candles[add_cursor].1.close_time <= chunk_end {
                add_cursor += 1;
            }

            // Compute total event count: each primary candle → 2 events (L1 + Candle),
            // each additional candle → 1 event (Candle only; primary L1 sets the price).
            let add_count = add_cursor - add_start;
            let mut tagged: Vec<(u64, MarketStreamEvent<InstrumentIndex, DataKind>)> =
                Vec::with_capacity(page.len() * 2 + add_count);

            // Additional-interval candle events (no preceding L1 — primary price is authoritative).
            for (add_secs, add_row) in &add_candles[add_start..add_cursor] {
                tagged.push((*add_secs, MarketStreamEvent::Item(MarketEvent {
                    time_exchange: add_row.close_time,
                    time_received: add_row.close_time,
                    exchange: exchange_id,
                    instrument: instrument_index,
                    kind: DataKind::Candle(Candle {
                        close_time: add_row.close_time,
                        open: add_row.open,
                        high: add_row.high,
                        low: add_row.low,
                        close: add_row.close,
                        volume: add_row.volume,
                        trade_count: add_row.trade_count as u64,
                    }),
                })));
            }

            // Primary-interval events (OrderBookL1 + Candle pair).
            for candle in &page {
                let close = Decimal::from_f64_retain(candle.close).unwrap_or_default();
                let level = barter_data::books::Level { price: close, amount: Decimal::ONE };
                // L1 carries primary_secs hint but is not a Candle event — hint will be reset
                // just before the actual Candle event below.
                tagged.push((primary_secs, MarketStreamEvent::Item(MarketEvent {
                    time_exchange: candle.close_time,
                    time_received: candle.close_time,
                    exchange: exchange_id,
                    instrument: instrument_index,
                    kind: DataKind::OrderBookL1(OrderBookL1 {
                        last_update_time: candle.close_time,
                        best_bid: Some(level),
                        best_ask: Some(level),
                    }),
                })));
                tagged.push((primary_secs, MarketStreamEvent::Item(MarketEvent {
                    time_exchange: candle.close_time,
                    time_received: candle.close_time,
                    exchange: exchange_id,
                    instrument: instrument_index,
                    kind: DataKind::Candle(Candle {
                        close_time: candle.close_time,
                        open: candle.open,
                        high: candle.high,
                        low: candle.low,
                        close: candle.close,
                        volume: candle.volume,
                        trade_count: candle.trade_count as u64,
                    }),
                })));
            }
            drop(page);

            // Stable sort by event timestamp so additional-interval candles are interleaved
            // at the correct chronological position relative to primary candles.
            tagged.sort_by_key(|(_, ev)| match ev {
                MarketStreamEvent::Item(m) => m.time_exchange,
                _ => DateTime::<Utc>::MAX_UTC,
            });

            // Process events one at a time, setting `next_candle_interval_hint` before
            // each DataKind::Candle so the processor routes it to the correct history.
            let mut chunk_positions = Vec::new();
            for (hint_secs, event) in &tagged {
                if matches!(event, MarketStreamEvent::Item(m) if matches!(m.kind, DataKind::Candle(_))) {
                    engine.instrument_data_mut(&instrument_index).next_candle_interval_hint = Some(*hint_secs);
                }
                engine.process_chunk(std::slice::from_ref(event), &strategy, &mut run_state, &mut chunk_positions);
            }
            drop(tagged);

            if !chunk_positions.is_empty() {
                let insert_positions: Vec<InsertPosition> = chunk_positions
                    .iter()
                    .enumerate()
                    .map(|(i, p)| {
                        let qty = p.quantity_abs_max;
                        let gross = p.pnl_realised + p.fees_enter.fees + p.fees_exit.fees;
                        let price_exit = if qty.is_zero() {
                            p.price_entry_average
                        } else {
                            match p.side {
                                barter_instrument::Side::Buy => p.price_entry_average + gross / qty,
                                barter_instrument::Side::Sell => p.price_entry_average - gross / qty,
                            }
                        };
                        InsertPosition {
                            seq: (total_position_count + i) as i32,
                            side: format!("{:?}", p.side),
                            price_entry: p.price_entry_average,
                            price_exit,
                            quantity: qty,
                            pnl: p.pnl_realised,
                            fees_enter: p.fees_enter.fees,
                            fees_exit: p.fees_exit.fees,
                            time_enter: p.time_enter,
                            time_exit: p.time_exit,
                        }
                    })
                    .collect();

                total_position_count += chunk_positions.len();

                if let Err(e) = paper_run_position::insert_batch(pool, run_id, &insert_positions).await {
                    warn!(%run_id, ?e, "failed to write positions chunk to paper_run_positions table");
                }
            }

            // Drain condition audit entries produced by this chunk.
            if let Some(ref audit_log) = audit_log {
                let mut drained: Vec<BarAudit> = {
                    let mut guard = audit_log.lock().unwrap();
                    guard.drain(..).collect()
                };
                if config.audit_conditions && !drained.is_empty() {
                    let insert_rows: Vec<InsertAuditRow> = drained.iter().map(|a| InsertAuditRow {
                        bar_index: a.bar_index as i32,
                        bar_time: a.bar_time,
                        rule_index: a.rule_index as i16,
                        rule_fired: a.rule_fired,
                        conditions: serde_json::to_value(&a.conditions)
                            .unwrap_or(serde_json::Value::Array(vec![])),
                    }).collect();
                    if let Err(e) = condition_audit::insert_batch(pool, run_id, &insert_rows).await {
                        warn!(%run_id, ?e, "failed to write condition audit chunk");
                    }
                }
                all_audits.append(&mut drained);
            }

            debug!(
                total_candle_count,
                total_position_count,
                "processed chunk of {fetched} candles"
            );

            if (fetched as i64) < PAGE_SIZE {
                break;
            }
            page = swym_dal::repo::market_event::load_candles_page(
                pool,
                instrument_row.id,
                interval,
                starts_at,
                finishes_at,
                last_id,
                PAGE_SIZE,
            )
            .await?;
        }

        let trading_summary = engine.finish_run(run_state);

        let assets: Vec<serde_json::Value> = trading_summary
            .assets
            .iter()
            .map(|(k, v)| serde_json::json!({ "exchange": k.exchange, "asset": k.asset, "tear_sheet": v }))
            .collect();

        info!(%run_id, total_candle_count, total_position_count, interval, "candle backtest complete");

        let condition_audit_summary = if !all_audits.is_empty() {
            Some(serde_json::to_value(compute_summary(&all_audits))
                .unwrap_or(serde_json::Value::Null))
        } else {
            None
        };

        let pos_stats =
            paper_run_position::aggregate_stats(pool, run_id).await.unwrap_or_default();
        let tear_sheet = trading_summary.instruments.values().next();
        let win_rate = tear_sheet.and_then(|t| t.win_rate.as_ref()).map(|w| w.value);
        let profit_factor = tear_sheet.and_then(|t| t.profit_factor.as_ref()).map(|p| p.value);
        let sharpe_ratio = tear_sheet.map(|t| t.sharpe_ratio.value);
        let sortino_ratio = tear_sheet.map(|t| t.sortino_ratio.value);
        let calmar_ratio = tear_sheet.map(|t| t.calmar_ratio.value);
        let max_drawdown = tear_sheet.and_then(|t| t.pnl_drawdown_max.as_ref()).map(|d| d.0.value);
        let pnl_return = tear_sheet.map(|t| t.pnl_return.value);

        return Ok(serde_json::json!({
            "total_positions": total_position_count,
            "winning_positions": pos_stats.winning,
            "losing_positions": pos_stats.losing,
            "win_rate": win_rate,
            "profit_factor": profit_factor,
            "total_pnl": pos_stats.total_pnl_net + pos_stats.total_fees,
            "total_fees": pos_stats.total_fees,
            "net_pnl": pos_stats.total_pnl_net,
            "sharpe_ratio": sharpe_ratio,
            "sortino_ratio": sortino_ratio,
            "calmar_ratio": calmar_ratio,
            "max_drawdown": max_drawdown,
            "pnl_return": pnl_return,
            "avg_win": pos_stats.avg_win,
            "avg_loss": pos_stats.avg_loss,
            "max_win": pos_stats.max_win,
            "max_loss": pos_stats.max_loss,
            "avg_hold_duration_secs": pos_stats.avg_hold_duration_secs,
            "time_engine_start": trading_summary.time_engine_start,
            "time_engine_end": trading_summary.time_engine_end,
            "instruments": trading_summary.instruments,
            "assets": assets,
            "backtest_metadata": {
                "candle_count": total_candle_count,
                "candle_interval": interval,
                "data_range_start": data_start,
                "data_range_end": data_end,
                "position_count": total_position_count,
            },
            "condition_audit_summary": condition_audit_summary,
        }));
    }

    // --- Tick-based backtest path (candle_interval is None) ---

    // Peek at the first page to get the first event timestamp and verify data exists.
    let first_page = swym_dal::repo::market_event::load_trades_page(
        pool,
        instrument_row.id,
        starts_at,
        finishes_at,
        0,
        PAGE_SIZE,
    )
    .await?;

    if first_page.is_empty() {
        return Err(format!(
            "no trade data found for {name_exchange} between {starts_at} and {finishes_at}"
        )
        .into());
    }

    let first_event_time = first_page[0].time_exchange;
    info!("beginning chunked backtest replay");

    let mut engine = BacktestEngine::<SwymInstrumentData>::new(
        instruments,
        balances,
        config.execution.fees_percent,
        risk_free_return,
        first_event_time,
    );

    let mut run_state = engine.begin_run();

    // Chunked replay: process one page at a time to bound memory usage.
    let mut last_id: i64 = 0;
    let mut total_trade_count: usize = 0;
    let mut total_position_count: usize = 0;
    let mut data_start: Option<DateTime<Utc>> = None;
    let mut data_end: Option<DateTime<Utc>> = None;
    let mut page = first_page;

    loop {
        if page.is_empty() {
            break;
        }

        if data_start.is_none() {
            data_start = Some(page[0].time_exchange);
        }
        data_end = page.last().map(|t| t.time_exchange);
        last_id = page.last().map(|t| t.id).unwrap_or(last_id);
        total_trade_count += page.len();
        let fetched = page.len();

        // Convert this page's trades to MarketStreamEvents.
        let mut events: Vec<MarketStreamEvent<InstrumentIndex, DataKind>> =
            Vec::with_capacity(page.len() * 2);

        for trade in &page {
            let side: Side = match trade.side.as_str() {
                "Buy" | "buy" | "BUY" => Side::Buy,
                _ => Side::Sell,
            };

            events.push(MarketStreamEvent::Item(MarketEvent {
                time_exchange: trade.time_exchange,
                time_received: trade.time_received,
                exchange: exchange_id,
                instrument: instrument_index,
                kind: DataKind::Trade(PublicTrade {
                    id: trade.trade_id.clone(),
                    price: trade.price,
                    amount: trade.amount,
                    side,
                }),
            }));

            let price = Decimal::try_from(trade.price)
                .unwrap_or_else(|_| Decimal::from_f64_retain(trade.price).unwrap_or_default());
            let amount = Decimal::try_from(trade.amount)
                .unwrap_or_else(|_| Decimal::from_f64_retain(trade.amount).unwrap_or_default());
            let level = Level { price, amount };
            let (best_bid, best_ask) = match side {
                Side::Sell => (Some(level), None),
                Side::Buy  => (None, Some(level)),
            };

            events.push(MarketStreamEvent::Item(MarketEvent {
                time_exchange: trade.time_exchange,
                time_received: trade.time_received,
                exchange: exchange_id,
                instrument: instrument_index,
                kind: DataKind::OrderBookL1(OrderBookL1 {
                    last_update_time: trade.time_exchange,
                    best_bid,
                    best_ask,
                }),
            }));
        }
        drop(page); // free trade rows before processing

        // Run strategy over this chunk, collecting completed positions.
        let mut chunk_positions = Vec::new();
        engine.process_chunk(&events, &strategy, &mut run_state, &mut chunk_positions);
        drop(events);

        // Write completed positions for this chunk to the DB immediately.
        if !chunk_positions.is_empty() {
            let insert_positions: Vec<InsertPosition> = chunk_positions
                .iter()
                .enumerate()
                .map(|(i, p)| {
                    let qty = p.quantity_abs_max;
                    let gross = p.pnl_realised + p.fees_enter.fees + p.fees_exit.fees;
                    let price_exit = if qty.is_zero() {
                        p.price_entry_average
                    } else {
                        match p.side {
                            barter_instrument::Side::Buy => p.price_entry_average + gross / qty,
                            barter_instrument::Side::Sell => p.price_entry_average - gross / qty,
                        }
                    };
                    InsertPosition {
                        seq: (total_position_count + i) as i32,
                        side: format!("{:?}", p.side),
                        price_entry: p.price_entry_average,
                        price_exit,
                        quantity: qty,
                        pnl: p.pnl_realised,
                        fees_enter: p.fees_enter.fees,
                        fees_exit: p.fees_exit.fees,
                        time_enter: p.time_enter,
                        time_exit: p.time_exit,
                    }
                })
                .collect();

            total_position_count += chunk_positions.len();

            if let Err(e) = paper_run_position::insert_batch(pool, run_id, &insert_positions).await {
                warn!(%run_id, ?e, "failed to write positions chunk to paper_run_positions table");
            }
        }

        // Drain condition audit entries produced by this chunk.
        if let Some(ref audit_log) = audit_log {
            let mut drained: Vec<BarAudit> = {
                let mut guard = audit_log.lock().unwrap();
                guard.drain(..).collect()
            };
            if config.audit_conditions && !drained.is_empty() {
                let insert_rows: Vec<InsertAuditRow> = drained.iter().map(|a| InsertAuditRow {
                    bar_index: a.bar_index as i32,
                    bar_time: a.bar_time,
                    rule_index: a.rule_index as i16,
                    rule_fired: a.rule_fired,
                    conditions: serde_json::to_value(&a.conditions)
                        .unwrap_or(serde_json::Value::Array(vec![])),
                }).collect();
                if let Err(e) = condition_audit::insert_batch(pool, run_id, &insert_rows).await {
                    warn!(%run_id, ?e, "failed to write condition audit chunk");
                }
            }
            all_audits.append(&mut drained);
        }

        debug!(
            total_trade_count,
            total_position_count,
            "processed chunk of {fetched} trades"
        );

        // Fetch the next page.
        if (fetched as i64) < PAGE_SIZE {
            break; // last page was partial — we're done
        }
        page = swym_dal::repo::market_event::load_trades_page(
            pool,
            instrument_row.id,
            starts_at,
            finishes_at,
            last_id,
            PAGE_SIZE,
        )
        .await?;
    }

    let trading_summary = engine.finish_run(run_state);

    let assets: Vec<serde_json::Value> = trading_summary
        .assets
        .iter()
        .map(|(k, v)| serde_json::json!({ "exchange": k.exchange, "asset": k.asset, "tear_sheet": v }))
        .collect();

    info!(%run_id, total_trade_count, total_position_count, "backtest complete");

    let condition_audit_summary = if !all_audits.is_empty() {
        Some(serde_json::to_value(compute_summary(&all_audits))
            .unwrap_or(serde_json::Value::Null))
    } else {
        None
    };

    let pos_stats = paper_run_position::aggregate_stats(pool, run_id).await.unwrap_or_default();
    let tear_sheet = trading_summary.instruments.values().next();
    let win_rate = tear_sheet.and_then(|t| t.win_rate.as_ref()).map(|w| w.value);
    let profit_factor = tear_sheet.and_then(|t| t.profit_factor.as_ref()).map(|p| p.value);
    let sharpe_ratio = tear_sheet.map(|t| t.sharpe_ratio.value);
    let sortino_ratio = tear_sheet.map(|t| t.sortino_ratio.value);
    let calmar_ratio = tear_sheet.map(|t| t.calmar_ratio.value);
    let max_drawdown = tear_sheet.and_then(|t| t.pnl_drawdown_max.as_ref()).map(|d| d.0.value);
    let pnl_return = tear_sheet.map(|t| t.pnl_return.value);

    let result = serde_json::json!({
        "total_positions": total_position_count,
        "winning_positions": pos_stats.winning,
        "losing_positions": pos_stats.losing,
        "win_rate": win_rate,
        "profit_factor": profit_factor,
        "total_pnl": pos_stats.total_pnl_net + pos_stats.total_fees,
        "total_fees": pos_stats.total_fees,
        "net_pnl": pos_stats.total_pnl_net,
        "sharpe_ratio": sharpe_ratio,
        "sortino_ratio": sortino_ratio,
        "calmar_ratio": calmar_ratio,
        "max_drawdown": max_drawdown,
        "pnl_return": pnl_return,
        "avg_win": pos_stats.avg_win,
        "avg_loss": pos_stats.avg_loss,
        "max_win": pos_stats.max_win,
        "max_loss": pos_stats.max_loss,
        "avg_hold_duration_secs": pos_stats.avg_hold_duration_secs,
        "time_engine_start": trading_summary.time_engine_start,
        "time_engine_end": trading_summary.time_engine_end,
        "instruments": trading_summary.instruments,
        "assets": assets,
        "backtest_metadata": {
            "trade_count": total_trade_count,
            "data_range_start": data_start,
            "data_range_end": data_end,
            "position_count": total_position_count,
        },
        "condition_audit_summary": condition_audit_summary,
    });

    Ok(result)
}

/// Load all candles for a given instrument/interval/time range in a single paginated sweep.
///
/// Used to prefetch additional-timeframe (e.g. 1h, 1d) candles before the main backtest loop.
/// These intervals have far fewer rows than the primary interval so loading them upfront is safe.
async fn load_all_candles(
    pool: &PgPool,
    instrument_id: i32,
    interval: &str,
    from: DateTime<Utc>,
    to: DateTime<Utc>,
) -> Result<Vec<swym_dal::models::market_event::MarketCandleRow>, Box<dyn std::error::Error + Send + Sync>>
{
    const PAGE: i64 = 50_000;
    let mut all = Vec::new();
    let mut last_id: i64 = 0;
    loop {
        let page = swym_dal::repo::market_event::load_candles_page(
            pool, instrument_id, interval, from, to, last_id, PAGE,
        ).await?;
        let fetched = page.len();
        if let Some(last) = page.last() {
            last_id = last.id;
        }
        all.extend(page);
        if (fetched as i64) < PAGE {
            break;
        }
    }
    Ok(all)
}

/// Extract exchange name and instrument exchange name from a `RunConfig.instrument` JSON blob.
fn extract_instrument_identifiers(
    instrument: &serde_json::Value,
) -> Result<(String, String), Box<dyn std::error::Error + Send + Sync>> {
    let exchange = instrument
        .get("exchange")
        .and_then(|v| v.as_str())
        .ok_or("instrument config missing 'exchange' field")?
        .to_string();
    let name_exchange = instrument
        .get("name_exchange")
        .and_then(|v| v.as_str())
        .ok_or("instrument config missing 'name_exchange' field")?
        .to_string();
    Ok((exchange, name_exchange))
}
