//! Order management types: limit entries, stop-loss, take-profit, trailing stops.
//!
//! All configuration types are `serde`-compatible and optional. When no order
//! features are configured, the backtest engine behaves identically to the
//! legacy market-order-only path (zero overhead).

use serde::{Deserialize, Serialize};

// ---------------------------------------------------------------------------
// Configuration types (serialised inside ExecutionConfig)
// ---------------------------------------------------------------------------

/// Time-in-force for limit entry orders.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum TimeInForce {
    /// Good-til-cancelled: order persists until filled or position signal changes.
    #[default]
    GTC,
    /// Good-til-bar(n): cancel after `n` bars if not filled.
    GTB(u32),
    /// Immediate-or-cancel: must fill on the arrival bar or cancel remainder.
    IOC,
}

/// Limit entry order configuration.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct LimitOrderConfig {
    /// Offset from signal-bar close in basis points.
    ///
    /// For **buy**: `limit_price = close * (1 - offset_bps / 10_000)`
    /// For **sell**: `limit_price = close * (1 + offset_bps / 10_000)`
    ///
    /// Positive offset = more passive (better price for you).
    pub offset_bps: f64,
    /// Time-in-force policy.
    #[serde(default)]
    pub time_in_force: TimeInForce,
}

/// Fixed stop-loss configuration (percentage from entry price).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct StopLossConfig {
    /// Distance from entry price as a percentage (e.g. `2.0` = 2%).
    pub stop_pct: f64,
}

/// Fixed take-profit configuration (percentage from entry price).
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TakeProfitConfig {
    /// Distance from entry price as a percentage (e.g. `5.0` = 5%).
    pub profit_pct: f64,
}

/// Trailing stop configuration.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct TrailingStopConfig {
    /// Trail distance as a percentage (e.g. `3.0` = 3%).
    pub trail_pct: f64,
    /// Track the bar high/low (`true`, default) or just the close (`false`).
    #[serde(default = "default_true")]
    pub use_high: bool,
}

fn default_true() -> bool {
    true
}

/// Top-level order management configuration.
///
/// All fields are `Option` so that when nothing is set the engine falls back
/// to the legacy market-order path with **zero overhead**.
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
pub struct OrderConfig {
    /// Limit entry order.  When set, signal-generated entry orders use a limit
    /// price instead of a market fill.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub limit_entry: Option<LimitOrderConfig>,

    /// Stop-loss on open positions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub stop_loss: Option<StopLossConfig>,

    /// Take-profit on open positions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub take_profit: Option<TakeProfitConfig>,

    /// Trailing stop on open positions.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trailing_stop: Option<TrailingStopConfig>,
}

impl OrderConfig {
    /// `true` when no order management features are enabled.
    #[inline]
    pub fn is_empty(&self) -> bool {
        self.limit_entry.is_none()
            && self.stop_loss.is_none()
            && self.take_profit.is_none()
            && self.trailing_stop.is_none()
    }

    /// `true` when any exit order (SL / TP / trailing) is configured.
    #[inline]
    pub fn has_exit_orders(&self) -> bool {
        self.stop_loss.is_some() || self.take_profit.is_some() || self.trailing_stop.is_some()
    }
}

// ---------------------------------------------------------------------------
// Runtime state — per-symbol bracket tracking
// ---------------------------------------------------------------------------

/// Why an exit trade was triggered.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ExitReason {
    /// Normal signal-driven position change (legacy behaviour).
    Signal = 0,
    StopLoss = 1,
    TakeProfit = 2,
    TrailingStop = 3,
    /// Limit entry expired unfilled.
    LimitExpiry = 4,
}

impl ExitReason {
    #[inline]
    pub fn as_u8(self) -> u8 {
        self as u8
    }
}

/// Active bracket (SL / TP / trailing) attached to a live position.
///
/// Created after an entry fill completes when [`OrderConfig::has_exit_orders`]
/// is `true`.  Destroyed when the position returns to zero or a new signal
/// overrides it.
#[derive(Clone, Debug)]
pub struct ActiveBracket {
    /// Average entry price of the position.
    pub entry_price: f64,
    /// `+1.0` for long, `-1.0` for short.
    pub position_sign: f64,
    /// Absolute stop-loss trigger price (`None` if no SL configured).
    pub stop_price: Option<f64>,
    /// Absolute take-profit trigger price (`None` if no TP configured).
    pub tp_price: Option<f64>,
    /// Current trailing-stop trigger price (ratchets in favour of the position).
    pub trailing_stop_price: Option<f64>,
    /// High-water-mark used for trailing stop calculation.
    pub trail_hwm: f64,
}

impl ActiveBracket {
    /// Compute bracket levels from the entry price and order config.
    ///
    /// Returns `None` when the config has no exit orders.
    pub fn new(entry_price: f64, position: f64, cfg: &OrderConfig) -> Option<Self> {
        if !cfg.has_exit_orders() {
            return None;
        }

        let sign = position.signum();

        let stop_price = cfg.stop_loss.as_ref().map(|sl| {
            if sign > 0.0 {
                entry_price * (1.0 - sl.stop_pct / 100.0)
            } else {
                entry_price * (1.0 + sl.stop_pct / 100.0)
            }
        });

        let tp_price = cfg.take_profit.as_ref().map(|tp| {
            if sign > 0.0 {
                entry_price * (1.0 + tp.profit_pct / 100.0)
            } else {
                entry_price * (1.0 - tp.profit_pct / 100.0)
            }
        });

        let trailing_stop_price = cfg.trailing_stop.as_ref().map(|ts| {
            if sign > 0.0 {
                entry_price * (1.0 - ts.trail_pct / 100.0)
            } else {
                entry_price * (1.0 + ts.trail_pct / 100.0)
            }
        });

        Some(Self {
            entry_price,
            position_sign: sign,
            stop_price,
            tp_price,
            trailing_stop_price,
            trail_hwm: entry_price,
        })
    }

    /// Update the trailing stop after observing a new bar.
    ///
    /// The high-water-mark ratchets in the direction of the position (up for
    /// longs, down for shorts) and the trailing stop follows.
    pub fn update_trail(&mut self, bar_high: f64, bar_low: f64, trail_pct: f64, use_high: bool) {
        if self.trailing_stop_price.is_none() {
            return;
        }

        if self.position_sign > 0.0 {
            // Long: track the highest point, trail below it.
            let reference = if use_high {
                bar_high
            } else {
                bar_high.min(bar_low).max(bar_high)
            };
            // ^ for close-mode the caller should pass close as bar_high; simplified here.
            if reference > self.trail_hwm {
                self.trail_hwm = reference;
                self.trailing_stop_price = Some(self.trail_hwm * (1.0 - trail_pct / 100.0));
            }
        } else {
            // Short: track the lowest point, trail above it.
            let reference = if use_high { bar_low } else { bar_low };
            if reference < self.trail_hwm {
                self.trail_hwm = reference;
                self.trailing_stop_price = Some(self.trail_hwm * (1.0 + trail_pct / 100.0));
            }
        }
    }

    /// Check whether a stop-loss or trailing stop was triggered on this bar.
    ///
    /// Uses intra-bar high/low for realistic trigger detection.
    /// Returns the trigger price if triggered, `None` otherwise.
    pub fn check_stop(&self, bar_high: f64, bar_low: f64) -> Option<(f64, ExitReason)> {
        // Fixed stop-loss (checked first).
        if let Some(stop) = self.stop_price {
            if self.position_sign > 0.0 && bar_low <= stop {
                return Some((stop, ExitReason::StopLoss));
            }
            if self.position_sign < 0.0 && bar_high >= stop {
                return Some((stop, ExitReason::StopLoss));
            }
        }
        // Trailing stop.
        if let Some(trail) = self.trailing_stop_price {
            if self.position_sign > 0.0 && bar_low <= trail {
                return Some((trail, ExitReason::TrailingStop));
            }
            if self.position_sign < 0.0 && bar_high >= trail {
                return Some((trail, ExitReason::TrailingStop));
            }
        }
        None
    }

    /// Check whether a take-profit was triggered on this bar.
    ///
    /// Returns the trigger price if triggered, `None` otherwise.
    pub fn check_tp(&self, bar_high: f64, bar_low: f64) -> Option<f64> {
        if let Some(tp) = self.tp_price {
            if self.position_sign > 0.0 && bar_high >= tp {
                return Some(tp);
            }
            if self.position_sign < 0.0 && bar_low <= tp {
                return Some(tp);
            }
        }
        None
    }
}

// ---------------------------------------------------------------------------
// Unit tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;

    fn cfg_sl(pct: f64) -> OrderConfig {
        OrderConfig {
            stop_loss: Some(StopLossConfig { stop_pct: pct }),
            ..Default::default()
        }
    }

    fn cfg_tp(pct: f64) -> OrderConfig {
        OrderConfig {
            take_profit: Some(TakeProfitConfig { profit_pct: pct }),
            ..Default::default()
        }
    }

    fn cfg_trailing(pct: f64) -> OrderConfig {
        OrderConfig {
            trailing_stop: Some(TrailingStopConfig {
                trail_pct: pct,
                use_high: true,
            }),
            ..Default::default()
        }
    }

    fn cfg_bracket(sl_pct: f64, tp_pct: f64) -> OrderConfig {
        OrderConfig {
            stop_loss: Some(StopLossConfig { stop_pct: sl_pct }),
            take_profit: Some(TakeProfitConfig { profit_pct: tp_pct }),
            ..Default::default()
        }
    }

    // --- OrderConfig ---

    #[test]
    fn order_config_default_is_empty() {
        let cfg = OrderConfig::default();
        assert!(cfg.is_empty());
        assert!(!cfg.has_exit_orders());
    }

    #[test]
    fn order_config_with_sl_is_not_empty() {
        let cfg = cfg_sl(2.0);
        assert!(!cfg.is_empty());
        assert!(cfg.has_exit_orders());
    }

    #[test]
    fn order_config_limit_only_not_exit() {
        let cfg = OrderConfig {
            limit_entry: Some(LimitOrderConfig {
                offset_bps: 10.0,
                time_in_force: TimeInForce::GTC,
            }),
            ..Default::default()
        };
        assert!(!cfg.is_empty());
        assert!(!cfg.has_exit_orders());
    }

    // --- ActiveBracket::new ---

    #[test]
    fn bracket_new_returns_none_when_empty() {
        let cfg = OrderConfig::default();
        assert!(ActiveBracket::new(100.0, 1.0, &cfg).is_none());
    }

    #[test]
    fn bracket_long_sl_price() {
        let cfg = cfg_sl(2.0);
        let b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();
        assert!((b.stop_price.unwrap() - 98.0).abs() < 1e-10);
        assert!(b.tp_price.is_none());
        assert!(b.trailing_stop_price.is_none());
    }

    #[test]
    fn bracket_short_sl_price() {
        let cfg = cfg_sl(2.0);
        let b = ActiveBracket::new(100.0, -1.0, &cfg).unwrap();
        assert!((b.stop_price.unwrap() - 102.0).abs() < 1e-10);
    }

    #[test]
    fn bracket_long_tp_price() {
        let cfg = cfg_tp(5.0);
        let b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();
        assert!((b.tp_price.unwrap() - 105.0).abs() < 1e-10);
        assert!(b.stop_price.is_none());
    }

    #[test]
    fn bracket_short_tp_price() {
        let cfg = cfg_tp(5.0);
        let b = ActiveBracket::new(100.0, -1.0, &cfg).unwrap();
        assert!((b.tp_price.unwrap() - 95.0).abs() < 1e-10);
    }

    #[test]
    fn bracket_combined_sl_tp() {
        let cfg = cfg_bracket(2.0, 5.0);
        let b = ActiveBracket::new(200.0, 1.0, &cfg).unwrap();
        assert!((b.stop_price.unwrap() - 196.0).abs() < 1e-10);
        assert!((b.tp_price.unwrap() - 210.0).abs() < 1e-10);
    }

    #[test]
    fn bracket_trailing_initial() {
        let cfg = cfg_trailing(3.0);
        let b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();
        assert!((b.trailing_stop_price.unwrap() - 97.0).abs() < 1e-10);
        assert!((b.trail_hwm - 100.0).abs() < 1e-10);
    }

    // --- update_trail ---

    #[test]
    fn trailing_long_ratchets_up() {
        let cfg = cfg_trailing(3.0);
        let mut b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();

        // Price rises to 110 — trail should move up.
        b.update_trail(110.0, 105.0, 3.0, true);
        assert!((b.trail_hwm - 110.0).abs() < 1e-10);
        assert!((b.trailing_stop_price.unwrap() - 110.0 * 0.97).abs() < 1e-10);

        // Price drops to 108 — trail must NOT move down.
        b.update_trail(108.0, 106.0, 3.0, true);
        assert!((b.trail_hwm - 110.0).abs() < 1e-10);
    }

    #[test]
    fn trailing_short_ratchets_down() {
        let cfg = cfg_trailing(3.0);
        let mut b = ActiveBracket::new(100.0, -1.0, &cfg).unwrap();

        // Price drops to 90 — trail should move down.
        b.update_trail(95.0, 90.0, 3.0, true);
        assert!((b.trail_hwm - 90.0).abs() < 1e-10);
        assert!((b.trailing_stop_price.unwrap() - 90.0 * 1.03).abs() < 1e-10);

        // Price rises to 92 — trail must NOT move up.
        b.update_trail(92.0, 91.0, 3.0, true);
        assert!((b.trail_hwm - 90.0).abs() < 1e-10);
    }

    // --- check_stop ---

    #[test]
    fn stop_long_triggers_on_low() {
        let cfg = cfg_sl(2.0);
        let b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();
        // Low touches stop at 98.0.
        let result = b.check_stop(101.0, 97.5);
        assert!(result.is_some());
        let (price, reason) = result.unwrap();
        assert!((price - 98.0).abs() < 1e-10);
        assert_eq!(reason, ExitReason::StopLoss);
    }

    #[test]
    fn stop_long_does_not_trigger_above() {
        let cfg = cfg_sl(2.0);
        let b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();
        assert!(b.check_stop(102.0, 99.0).is_none());
    }

    #[test]
    fn stop_short_triggers_on_high() {
        let cfg = cfg_sl(2.0);
        let b = ActiveBracket::new(100.0, -1.0, &cfg).unwrap();
        let result = b.check_stop(103.0, 99.0);
        assert!(result.is_some());
        let (price, reason) = result.unwrap();
        assert!((price - 102.0).abs() < 1e-10);
        assert_eq!(reason, ExitReason::StopLoss);
    }

    #[test]
    fn trailing_stop_triggers() {
        let cfg = cfg_trailing(3.0);
        let mut b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();
        b.update_trail(110.0, 105.0, 3.0, true);
        // Trailing stop at 110 * 0.97 = 106.7
        let result = b.check_stop(107.0, 106.0);
        assert!(result.is_some());
        let (_, reason) = result.unwrap();
        assert_eq!(reason, ExitReason::TrailingStop);
    }

    // --- check_tp ---

    #[test]
    fn tp_long_triggers_on_high() {
        let cfg = cfg_tp(5.0);
        let b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();
        let result = b.check_tp(106.0, 99.0);
        assert!(result.is_some());
        assert!((result.unwrap() - 105.0).abs() < 1e-10);
    }

    #[test]
    fn tp_long_does_not_trigger_below() {
        let cfg = cfg_tp(5.0);
        let b = ActiveBracket::new(100.0, 1.0, &cfg).unwrap();
        assert!(b.check_tp(104.0, 99.0).is_none());
    }

    #[test]
    fn tp_short_triggers_on_low() {
        let cfg = cfg_tp(5.0);
        let b = ActiveBracket::new(100.0, -1.0, &cfg).unwrap();
        let result = b.check_tp(101.0, 94.0);
        assert!(result.is_some());
        assert!((result.unwrap() - 95.0).abs() < 1e-10);
    }

    // --- ExitReason ---

    #[test]
    fn exit_reason_values() {
        assert_eq!(ExitReason::Signal.as_u8(), 0);
        assert_eq!(ExitReason::StopLoss.as_u8(), 1);
        assert_eq!(ExitReason::TakeProfit.as_u8(), 2);
        assert_eq!(ExitReason::TrailingStop.as_u8(), 3);
        assert_eq!(ExitReason::LimitExpiry.as_u8(), 4);
    }

    // --- Serde round-trip ---

    #[test]
    fn order_config_serde_roundtrip() {
        let cfg = OrderConfig {
            limit_entry: Some(LimitOrderConfig {
                offset_bps: 10.0,
                time_in_force: TimeInForce::GTB(5),
            }),
            stop_loss: Some(StopLossConfig { stop_pct: 2.0 }),
            take_profit: Some(TakeProfitConfig { profit_pct: 5.0 }),
            trailing_stop: Some(TrailingStopConfig {
                trail_pct: 3.0,
                use_high: false,
            }),
        };
        let json = serde_json::to_string(&cfg).unwrap();
        let restored: OrderConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(cfg, restored);
    }

    #[test]
    fn order_config_empty_json_roundtrip() {
        let cfg = OrderConfig::default();
        let json = serde_json::to_string(&cfg).unwrap();
        assert_eq!(json, "{}");
        let restored: OrderConfig = serde_json::from_str(&json).unwrap();
        assert_eq!(cfg, restored);
    }
}
