//! Runtime-interpreted rule-based strategy.
//!
//! Evaluates a declarative JSON rule list on every candle close, placing market
//! orders for all rules whose `when` condition is satisfied.  The strategy never
//! fires between candle closes (`candle_ready` acts as the gate).
//!
//! # Backtrader equivalent
//!
//! There is no direct equivalent; the closest is a custom `Strategy` class that
//! checks a list of indicator conditions in `next()`.

use barter::{
    engine::Engine,
    engine::state::instrument::filter::InstrumentFilter,
    strategy::{
        algo::AlgoStrategy,
        close_positions::{ClosePositionsStrategy, close_open_positions_with_market_orders},
        on_disconnect::OnDisconnectStrategy,
        on_trading_disabled::OnTradingDisabled,
    },
};
use barter_execution::order::{
    id::{ClientOrderId, StrategyId},
    request::{OrderRequestCancel, OrderRequestOpen, RequestOpen},
    OrderEvent, OrderKey, OrderKind, TimeInForce,
};
use std::sync::{Arc, atomic::{AtomicU32, Ordering}};
use barter::engine::state::instrument::data::InstrumentDataState;
use barter_instrument::{
    Side,
    asset::AssetIndex,
    exchange::{ExchangeId, ExchangeIndex},
    instrument::InstrumentIndex,
};
use rust_decimal::Decimal;
use swym_dal::models::strategy_config::{ActionSide, QuantitySpec, Rule, SizingMethod};

use crate::strategy::{
    SwymState,
    audit::{AuditLog, BarAudit},
    signal::{EvalCtx, collect_ema_registrations, eval_expr, evaluate, evaluate_with_audit},
};

const STRATEGY_NAME: &str = "rule_based";

/// Runtime-interpreted rule-based strategy.
///
/// Constructed from [`RuleBasedParams`] via [`SwymStrategy::from_config`].  All
/// rules whose `when` condition evaluates to `true` at candle close simultaneously
/// fire their `then` action (no throttling; candle close is the natural throttle).
#[derive(Debug, Clone)]
pub struct RuleStrategy {
    pub id: StrategyId,
    /// Primary candle interval in seconds; drives `candle_ready` and strategy cadence.
    pub interval_secs: u64,
    /// Additional timeframes (interval_secs) referenced by expressions in the rule tree.
    /// These are tracked in parallel but do not gate strategy evaluation.
    pub additional_interval_secs: Vec<u64>,
    pub rules: Vec<Rule>,
    /// When `Some`, every candle-close evaluation is recorded here for condition auditing.
    /// Uses `Arc<Mutex<>>` for interior mutability through `&self` in `generate_algo_orders`.
    pub audit_log: Option<AuditLog>,
    /// Monotonically increasing counter incremented on each candle-close evaluation.
    /// Shared via `Arc` so `Clone` is cheap.
    pub bar_counter: Arc<AtomicU32>,
}

impl RuleStrategy {
    pub fn new(rules: Vec<Rule>, interval_secs: u64, additional_interval_secs: Vec<u64>) -> Self {
        Self {
            id: StrategyId::new(STRATEGY_NAME),
            interval_secs,
            additional_interval_secs,
            rules,
            audit_log: None,
            bar_counter: Arc::new(AtomicU32::new(0)),
        }
    }

    /// All (interval_secs, period) EMA pairs referenced anywhere in the rule tree.
    ///
    /// The executor uses this to pre-register [`RunningEma`] instances keyed by timeframe
    /// before the first candle arrives so that crossover detection has a valid `prev` value.
    pub fn ema_registrations(&self) -> Vec<(u64, usize)> {
        let mut regs = Vec::new();
        for rule in &self.rules {
            collect_ema_registrations(&rule.when, self.interval_secs, &mut regs);
        }
        regs.sort_unstable();
        regs.dedup();
        regs
    }
}


/// Resolve a [`SizingMethod`] to a base-asset quantity using the current price and balance map.
///
/// Returns `None` if `price` is zero or if a required balance is absent.
fn resolve_sizing(
    method: &SizingMethod,
    price: Decimal,
    balances: &std::collections::HashMap<String, Decimal>,
) -> Option<Decimal> {
    if price.is_zero() {
        return None;
    }
    match method {
        SizingMethod::FixedSum { amount } => {
            Some((*amount / price).max(Decimal::ZERO))
        }
        SizingMethod::PercentOfBalance { percent, asset } => {
            let balance = balances.get(asset.to_lowercase().as_str()).copied()?;
            let notional = balance * percent / Decimal::ONE_HUNDRED;
            Some((notional / price).max(Decimal::ZERO))
        }
        SizingMethod::FixedUnits { units } => {
            Some((*units).max(Decimal::ZERO))
        }
    }
}

impl AlgoStrategy for RuleStrategy {
    type State = SwymState;

    fn generate_algo_orders(
        &self,
        state: &Self::State,
    ) -> (
        impl IntoIterator<Item = OrderRequestCancel>,
        impl IntoIterator<Item = OrderRequestOpen>,
    ) {
        let cancels: Vec<OrderRequestCancel> = Vec::new();
        let mut opens: Vec<OrderRequestOpen> = Vec::new();

        // Build a free-balance map: internal asset name (lowercased) → free amount.
        let balances: std::collections::HashMap<String, Decimal> = state.assets.0.iter()
            .filter_map(|(key, asset_state)| {
                let free = asset_state.balance.as_ref()?.value.free;
                Some((key.asset.as_ref().to_lowercase(), free))
            })
            .collect();

        for instrument_state in state.instruments.instruments(&InstrumentFilter::None) {
            // Only evaluate when a candle has just closed.
            if !instrument_state.data.candle_ready {
                continue;
            }

            let Some(price) = instrument_state.data.price() else {
                continue;
            };

            // Extract position fields for the DSL expressions.
            let (entry_price, position_quantity, unrealised_pnl, time_enter) =
                instrument_state.position.current.as_ref().map(|p| (
                    Some(p.price_entry_average),
                    Some(p.quantity_abs),
                    Some(p.pnl_unrealised),
                    Some(p.time_enter),
                )).unwrap_or((None, None, None, None));
            let current_time = instrument_state.data.last_event_time;

            let ctx = EvalCtx {
                data: &instrument_state.data,
                is_flat: instrument_state.position.current.is_none(),
                is_long: instrument_state
                    .position
                    .current
                    .as_ref()
                    .is_some_and(|p| p.side == Side::Buy),
                is_short: instrument_state
                    .position
                    .current
                    .as_ref()
                    .is_some_and(|p| p.side == Side::Sell),
                primary_interval_secs: instrument_state.data.primary_interval_secs.unwrap_or(0),
                entry_price,
                position_quantity,
                unrealised_pnl,
                time_enter,
                current_time,
                balances: Some(&balances),
            };

            for (rule_index, rule) in self.rules.iter().enumerate() {
                let fired = if let Some(ref audit_log) = self.audit_log {
                    let bar_index = self.bar_counter.fetch_add(1, Ordering::Relaxed);
                    let bar_time = instrument_state.data.last_event_time
                        .unwrap_or_else(chrono::Utc::now);
                    let mut conditions = Vec::new();
                    let result = evaluate_with_audit(&rule.when, &ctx, "when", &mut conditions);
                    audit_log.lock().unwrap().push(BarAudit {
                        bar_index,
                        bar_time,
                        rule_index: rule_index as u8,
                        rule_comment: rule.comment.clone(),
                        rule_fired: result,
                        conditions,
                    });
                    result
                } else {
                    evaluate(&rule.when, &ctx)
                };

                if !fired {
                    continue;
                }

                let side = match rule.then.side {
                    ActionSide::Buy => Side::Buy,
                    ActionSide::Sell => Side::Sell,
                };

                // Resolve quantity: fixed, declarative sizing method, or dynamic expression.
                let base_quantity = match &rule.then.quantity {
                    QuantitySpec::Fixed(d) => Some(*d),
                    QuantitySpec::Sizing(s) => resolve_sizing(s, price, &balances),
                    QuantitySpec::Expr(e) => eval_expr(e, &ctx).map(|v| v.max(Decimal::ZERO)),
                };

                // Determine order quantity.
                //
                // - Opposite position + reverse=true:  close existing + open new (flip through
                //   zero). Quantity = position_qty + configured_qty. Barter splits the fill
                //   proportionally, closing the old position and opening the new one.
                // - Opposite position + reverse=false: close only. Quantity = position_qty,
                //   ignoring the configured quantity.
                // - No position / same side: use the configured base_quantity.
                let quantity = match instrument_state.position.current.as_ref() {
                    Some(pos) if pos.side != side && rule.then.reverse => {
                        let Some(q) = base_quantity else { continue };
                        pos.quantity_abs + q
                    }
                    Some(pos) if pos.side != side => pos.quantity_abs,
                    _ => {
                        let Some(q) = base_quantity else { continue };
                        q
                    }
                };

                opens.push(OrderEvent {
                    key: OrderKey {
                        exchange: instrument_state.instrument.exchange,
                        instrument: instrument_state.key,
                        strategy: self.id.clone(),
                        cid: ClientOrderId::random(),
                    },
                    state: RequestOpen {
                        side,
                        price,
                        quantity,
                        kind: OrderKind::Market,
                        time_in_force: TimeInForce::GoodUntilCancelled { post_only: false },
                    },
                });
            }
        }

        (cancels, opens)
    }
}

impl ClosePositionsStrategy for RuleStrategy {
    type State = SwymState;

    fn close_positions_requests<'a>(
        &'a self,
        state: &'a Self::State,
        filter: &'a InstrumentFilter,
    ) -> (
        impl IntoIterator<Item = OrderRequestCancel<ExchangeIndex, InstrumentIndex>> + 'a,
        impl IntoIterator<Item = OrderRequestOpen<ExchangeIndex, InstrumentIndex>> + 'a,
    )
    where
        ExchangeIndex: 'a,
        AssetIndex: 'a,
        InstrumentIndex: 'a,
    {
        close_open_positions_with_market_orders(&self.id, state, filter, |_| {
            ClientOrderId::random()
        })
    }
}

impl<Clock, State, ExecutionTxs, Risk> OnDisconnectStrategy<Clock, State, ExecutionTxs, Risk>
    for RuleStrategy
{
    type OnDisconnect = ();

    fn on_disconnect(
        _engine: &mut Engine<Clock, State, ExecutionTxs, Self, Risk>,
        _exchange: ExchangeId,
    ) {
    }
}

impl<Clock, State, ExecutionTxs, Risk> OnTradingDisabled<Clock, State, ExecutionTxs, Risk>
    for RuleStrategy
{
    type OnTradingDisabled = ();

    fn on_trading_disabled(
        _engine: &mut Engine<Clock, State, ExecutionTxs, Self, Risk>,
    ) {
    }
}
