use barter::{
    engine::{
        Processor,
        state::{
            EngineState, global::DefaultGlobalData, instrument::data::InstrumentDataState,
            order::in_flight_recorder::InFlightRequestRecorder, position::PositionExited,
            trading::TradingState,
        },
    },
    statistic::{summary::TradingSummaryGenerator, time::Daily},
    strategy::algo::AlgoStrategy,
};
use barter_data::event::{DataKind, MarketEvent};
use barter_execution::balance::Balance;
use barter_instrument::{
    Keyed,
    asset::{ExchangeAsset, QuoteAsset, name::AssetNameInternal},
    exchange::ExchangeIndex,
    index::IndexedInstruments,
    instrument::InstrumentIndex,
};
use barter_integration::snapshot::Snapshot;
use chrono::{DateTime, Utc};
use rust_decimal::Decimal;
use tracing::debug;

use crate::{BacktestError, fill};

/// Combined output of a backtest run.
pub struct BacktestResult {
    /// Aggregate statistics (tear sheets, ratios, PnL) per instrument and asset.
    pub summary: barter::statistic::summary::TradingSummary<Daily>,
    /// Every completed (entry + exit) position, in chronological order.
    pub positions: Vec<PositionExited<QuoteAsset, InstrumentIndex>>,
}

/// Accumulated state for an in-progress chunked backtest.
///
/// Obtained from [`BacktestEngine::begin_run`]; pass to [`BacktestEngine::process_chunk`]
/// repeatedly, then finalise with [`BacktestEngine::finish_run`].
pub struct RunState {
    pub summary_gen: TradingSummaryGenerator,
}

/// Synchronous backtest engine.
///
/// Replays historical market events one-by-one. After each event the strategy is consulted and
/// any orders are filled immediately at the requested price, with fees applied. This eliminates
/// the async timing issues inherent in barter's `MockExchange` pipeline.
pub struct BacktestEngine<InstrumentData> {
    pub state: EngineState<DefaultGlobalData, InstrumentData>,
    pub instruments: IndexedInstruments,
    pub fees_percent: Decimal,
    pub risk_free_return: Decimal,
    pub time_start: DateTime<Utc>,
    order_sequence: u64,
}

impl<InstrumentData> BacktestEngine<InstrumentData>
where
    InstrumentData: Default,
{
    /// Construct a `BacktestEngine` from an `IndexedInstruments`, initial balances, and config.
    ///
    /// `time_start` should be the `time_exchange` of the first market event.
    pub fn new(
        instruments: IndexedInstruments,
        balances: impl IntoIterator<Item = Keyed<ExchangeAsset<AssetNameInternal>, Balance>>,
        fees_percent: Decimal,
        risk_free_return: Decimal,
        time_start: DateTime<Utc>,
    ) -> Self {
        Self::new_with_init(instruments, balances, fees_percent, risk_free_return, time_start, |_| {
            InstrumentData::default()
        })
    }
}

impl<InstrumentData> BacktestEngine<InstrumentData> {
    /// Construct a `BacktestEngine` with a custom per-instrument data initialiser.
    ///
    /// Use this when instrument data requires configuration beyond `Default`, such as
    /// pre-registering stateful indicator periods (e.g. `RunningEma` periods).
    pub fn new_with_init(
        instruments: IndexedInstruments,
        balances: impl IntoIterator<Item = Keyed<ExchangeAsset<AssetNameInternal>, Balance>>,
        fees_percent: Decimal,
        risk_free_return: Decimal,
        time_start: DateTime<Utc>,
        init: impl Fn(&barter_instrument::Keyed<InstrumentIndex, barter_instrument::instrument::Instrument<barter_instrument::Keyed<ExchangeIndex, barter_instrument::exchange::ExchangeId>, barter_instrument::asset::AssetIndex>>) -> InstrumentData,
    ) -> Self {
        let state = EngineState::builder(&instruments, DefaultGlobalData::default(), init)
            .trading_state(TradingState::Enabled)
            .time_engine_start(time_start)
            .balances(balances)
            .build();

        Self {
            state,
            instruments,
            fees_percent,
            risk_free_return,
            time_start,
            order_sequence: 0,
        }
    }
}

impl<InstrumentData> BacktestEngine<InstrumentData>
where
    InstrumentData: InstrumentDataState<MarketEventKind = DataKind>
        + InFlightRequestRecorder<ExchangeIndex, InstrumentIndex>,
{
    /// Mutable access to a single instrument's data, identified by `InstrumentIndex`.
    ///
    /// Used by callers that need to set per-event side-channel fields (e.g.
    /// `next_candle_interval_hint`) between individual `process_chunk` calls.
    pub fn instrument_data_mut(&mut self, idx: &InstrumentIndex) -> &mut InstrumentData {
        &mut self.state.instruments.instrument_index_mut(idx).data
    }

    /// Begin a chunked backtest run, returning a [`RunState`] to pass to [`Self::process_chunk`].
    pub fn begin_run(&self) -> RunState {
        RunState {
            summary_gen: TradingSummaryGenerator::init(
                self.risk_free_return,
                self.time_start,
                self.time_start,
                &self.state.instruments,
                &self.state.assets,
            ),
        }
    }

    /// Process one chunk of market events, appending any completed positions to `positions`.
    ///
    /// Returns `Err(BacktestError::NoEvents)` only if `events` is completely empty.
    /// An empty-but-non-error return is fine for the last (partial) chunk.
    pub fn process_chunk<S>(
        &mut self,
        events: &[barter_data::streams::consumer::MarketStreamEvent<InstrumentIndex, DataKind>],
        strategy: &S,
        run_state: &mut RunState,
        positions: &mut Vec<PositionExited<QuoteAsset, InstrumentIndex>>,
    ) where
        S: AlgoStrategy<State = EngineState<DefaultGlobalData, InstrumentData>>,
        DefaultGlobalData: for<'a> Processor<&'a MarketEvent<InstrumentIndex, DataKind>>,
    {
        for event in events {
            let market_event = match event {
                barter_data::streams::consumer::MarketStreamEvent::Item(item) => item,
                _ => continue,
            };

            let time_exchange = market_event.time_exchange;

            self.state.update_from_market(market_event);

            let opens: Vec<_> = {
                let (_cancels, opens) = strategy.generate_algo_orders(&self.state);
                opens.into_iter().collect()
            };

            self.state.record_in_flight_opens(opens.iter());

            for open_request in opens {
                let instrument_idx = open_request.key.instrument;

                let instrument_state = self.state.instruments.instrument_index(&instrument_idx);

                let fill_result = fill::simulate_market_fill(
                    &open_request,
                    instrument_state,
                    &self.state.assets,
                    self.fees_percent,
                    time_exchange,
                    &mut self.order_sequence,
                );

                let Some(fill) = fill_result else {
                    debug!(
                        ?instrument_idx,
                        side = ?open_request.state.side,
                        "insufficient balance for order, skipping"
                    );
                    continue;
                };

                let instrument_state = self.state.instruments.instrument_index_mut(&instrument_idx);
                if let Some(exited) = instrument_state.update_from_trade(&fill.trade) {
                    run_state.summary_gen.update_from_position(&exited);
                    positions.push(exited);
                }

                self.state
                    .assets
                    .asset_index_mut(&fill.spent_balance.asset)
                    .update_from_balance(Snapshot(&fill.spent_balance));
                self.state
                    .assets
                    .asset_index_mut(&fill.received_balance.asset)
                    .update_from_balance(Snapshot(&fill.received_balance));

                run_state.summary_gen.update_from_balance(Snapshot(&fill.spent_balance));
                run_state.summary_gen.update_from_balance(Snapshot(&fill.received_balance));
            }

            run_state.summary_gen.update_time_now(time_exchange);
        }
    }

    /// Finalise a chunked run and return the trading summary.
    pub fn finish_run(&self, run_state: RunState) -> barter::statistic::summary::TradingSummary<Daily> {
        let mut run_state = run_state;
        run_state.summary_gen.generate(Daily)
    }

    /// Convenience: run the backtest over a complete slice of events in one call.
    pub fn run<S>(
        &mut self,
        events: &[barter_data::streams::consumer::MarketStreamEvent<InstrumentIndex, DataKind>],
        strategy: &S,
    ) -> Result<BacktestResult, BacktestError>
    where
        S: AlgoStrategy<State = EngineState<DefaultGlobalData, InstrumentData>>,
        DefaultGlobalData: for<'a> Processor<&'a MarketEvent<InstrumentIndex, DataKind>>,
    {
        if events.is_empty() {
            return Err(BacktestError::NoEvents);
        }

        let mut run_state = self.begin_run();
        let mut positions = Vec::new();
        self.process_chunk(events, strategy, &mut run_state, &mut positions);
        let summary = self.finish_run(run_state);

        Ok(BacktestResult { summary, positions })
    }
}
