//! Cross-matching engine for multi-runner markets.
//!
//! Cross-matching allows the exchange to fill user orders by creating
//! offsetting positions on other runners. The exchange uses an internal
//! house account to execute hedge legs, ensuring risk stays within
//! configured tolerance.
//!
//! ## Compensation Model
//!
//! Cross-matching follows an **all-or-nothing** model with void-on-failure:
//!
//! 1. The engine attempts to match the user's order and execute hedge legs
//! 2. If any hedge leg fails, all successful trades are voided
//! 3. Events from both the trades and voids are returned in `partial_events`
//!
//! This ensures the house never holds an unhedged position due to partial
//! cross-match failures.
//!
//! ## Risk Tolerance
//!
//! The [`RiskTolerance`] configuration controls how much worst-case loss the
//! house is willing to accept per cross-match:
//!
//! - **Risk-free**: Only accept perfectly hedged positions (worst-case P&L = 0)
//! - **Moderate**: Allow small losses to enable more matches
//! - **Custom**: Specify absolute and/or percentage-based tolerance
//!
//! Higher tolerance enables more cross-matches but increases house exposure.
//!
//! ## Example
//!
//! ```ignore
//! use btx::cross_match::{CrossMatchEngine, CrossMatchConfig, RiskTolerance};
//!
//! let config = CrossMatchConfig {
//!     risk: RiskTolerance::risk_free(),
//!     ..Default::default()
//! };
//! let mut engine = CrossMatchEngine::new(config);
//!
//! // After a user order rests without matching...
//! match engine.attempt_cross_match(&mut book, user_order_id) {
//!     CrossMatchResult::Success { events, hedge_legs, worst_case_pnl } => {
//!         // Cross-match succeeded - events include user match + hedge trades
//!         journal_events(&events);
//!     }
//!     CrossMatchResult::Failed { reason, partial_events } => {
//!         // Hedge leg failed - partial_events include matched trades AND their voids
//!         journal_events(&partial_events);
//!     }
//!     CrossMatchResult::NotPossible { reason } => {
//!         // No cross-match possible (insufficient liquidity, odds don't work, etc.)
//!     }
//! }
//! ```

mod config;
mod hedge;

pub use config::{CrossMatchConfig, RiskTolerance};
pub use hedge::{HedgeInput, HedgeLeg, HedgeResult, calculate_3runner_hedge};

use crate::book::protocol::command::{Command, CommandKind, Persistence, Side, TimeInForce};
use crate::book::{Book, BookEvent, BookEventEnvelope};
use crate::types::{AccountId, DateTime, MarketId, Money, OddsX10000, OrderId, RunnerId};

/// Result of a cross-match attempt.
#[derive(Debug, Clone)]
pub enum CrossMatchResult {
    /// Successfully executed cross-match.
    Success {
        /// All events from the cross-match (user match + hedge legs).
        events: Vec<BookEventEnvelope>,
        /// The hedge legs that were executed.
        hedge_legs: Vec<HedgeLeg>,
        /// Worst-case P&L for the house.
        worst_case_pnl: Money,
    },
    /// No cross-match possible (order remains resting).
    NotPossible { reason: &'static str },
    /// Cross-match attempted but a leg failed.
    Failed {
        reason: &'static str,
        /// Events from legs that did execute (may need compensation).
        partial_events: Vec<BookEventEnvelope>,
    },
}

/// Cross-matching engine.
///
/// Sits above the Book and orchestrates multi-leg cross-match execution.
/// The controller is responsible for transaction semantics (journaling, atomicity).
pub struct CrossMatchEngine {
    config: CrossMatchConfig,
    /// Next correlation id for hedge orders.
    next_correlation_id: u64,
    // TODO: Track aggregate exposure for circuit breaker
    // current_exposure: Money,
}

impl CrossMatchEngine {
    /// Create a new cross-match engine with the given configuration.
    pub fn new(config: CrossMatchConfig) -> Self {
        Self {
            config,
            // Start with high IDs to avoid collision with user orders
            next_correlation_id: 1_000_000_000,
        }
    }

    /// Get the house account ID.
    pub fn house_account(&self) -> AccountId {
        self.config.house_account.clone()
    }

    /// Attempt to cross-match a resting user order.
    ///
    /// Supports both BACK and LAY orders:
    /// - BACK: House LAYs target runner, then LAYs other runners as hedge
    /// - LAY: House BACKs target runner, then BACKs other runners as hedge
    ///
    /// # Arguments
    /// * `book` - The order book (will be mutated if cross-match succeeds)
    /// * `user_order_id` - The resting order to cross-match
    ///
    /// # Returns
    /// Result indicating success, not possible, or failure.
    pub fn attempt_cross_match(
        &mut self,
        book: &mut Book,
        user_order_id: OrderId,
    ) -> CrossMatchResult {
        // Get the user's order
        let Some(user_order) = book.get_order(user_order_id) else {
            return CrossMatchResult::NotPossible {
                reason: "Order not found",
            };
        };

        // Check order is still resting
        if !book.is_resting(user_order_id) {
            return CrossMatchResult::NotPossible {
                reason: "Order is not resting",
            };
        }

        let target_runner = user_order.runner_id;
        let user_side = user_order.info.side;
        let user_odds = user_order.price;
        let user_stake = user_order.remaining();

        // Get all runners in the market
        let runners: Vec<RunnerId> = book.runners().collect();

        // Enforce deployment cap first, then v1 algorithm constraint.
        if runners.len() > self.config.max_runners {
            return CrossMatchResult::NotPossible {
                reason: "Too many runners for cross-matching",
            };
        }
        if runners.len() != 3 {
            return CrossMatchResult::NotPossible {
                reason: "Cross-matching only supported for 3-runner markets",
            };
        }

        // Get other runners and their best prices for hedging
        // - For BACK orders: we need BACK liquidity on other runners (house will LAY against it)
        // - For LAY orders: we need LAY liquidity on other runners (house will BACK against it)
        let mut other_runners: Vec<(RunnerId, OddsX10000, Money)> = Vec::new();

        for &runner_id in &runners {
            if runner_id == target_runner {
                continue;
            }

            let best_price = match user_side {
                Side::Yes => {
                    // House needs to LAY other runners, so find BACK orders to match against
                    book.best_lay_price(runner_id)
                }
                Side::No => {
                    // House needs to BACK other runners, so find LAY orders to match against
                    book.best_back_price(runner_id)
                }
            };

            let Some(price_size) = best_price else {
                let reason = match user_side {
                    Side::Yes => "No BACK liquidity on other runners",
                    Side::No => "No LAY liquidity on other runners",
                };
                return CrossMatchResult::NotPossible { reason };
            };

            other_runners.push((runner_id, price_size.price, price_size.size));
        }

        // Calculate hedge
        let tolerance = self.config.risk.effective_tolerance(user_stake);
        let hedge_input = HedgeInput {
            target_runner,
            user_side,
            user_odds,
            user_stake,
            other_runners,
            max_loss: tolerance,
        };

        let hedge_result = calculate_3runner_hedge(&hedge_input);

        let (hedge_legs_raw, worst_case_pnl) = match hedge_result {
            HedgeResult::Success {
                legs,
                worst_case_pnl,
            } => (legs, worst_case_pnl),
            HedgeResult::NoSolution { reason } => {
                return CrossMatchResult::NotPossible { reason };
            }
        };
        // Zero-size hedge legs are valid solver output (e.g. already within tolerance),
        // but should not be sent as PlaceOrder commands.
        let hedge_legs: Vec<HedgeLeg> = hedge_legs_raw
            .into_iter()
            .filter(|leg| leg.stake.is_positive())
            .collect();

        // TODO: Check aggregate exposure circuit breaker
        // if self.current_exposure + worst_case_pnl.abs() > self.config.risk.max_total_exposure {
        //     return CrossMatchResult::NotPossible {
        //         reason: "Aggregate exposure limit reached",
        //     };
        // }

        // Build commands for execution
        let market_id = book.market_id();
        let mut commands = Vec::new();

        // 1. House takes opposite side of user's order
        let house_match_side = match user_side {
            Side::Yes => Side::No, // User backs, house lays
            Side::No => Side::Yes, // User lays, house backs
        };

        commands.push(self.make_place_order_cmd(
            market_id,
            target_runner,
            house_match_side,
            user_odds,
            user_stake,
        ));

        // 2. House places hedge legs (side comes from hedge calculation)
        for leg in &hedge_legs {
            commands.push(self.make_place_order_cmd(
                market_id,
                leg.runner_id,
                leg.side,
                leg.odds,
                leg.stake,
            ));
        }

        // Execute all commands sequentially, applying emitted events after each leg.
        let mut all_events: Vec<BookEventEnvelope> = Vec::new();
        for cmd in commands {
            match book.handle(&cmd) {
                Ok((events, _)) => {
                    book.apply_all_events(&events);
                    all_events.extend(events);
                }
                Err(_) => {
                    let compensation_failed = if let Some(from_matched_at_inclusive) =
                        extract_first_trade_match_ts(&all_events)
                    {
                        self.append_compensation_void(
                            book,
                            market_id,
                            from_matched_at_inclusive,
                            &mut all_events,
                        )
                        .is_err()
                    } else {
                        false
                    };
                    return CrossMatchResult::Failed {
                        reason: if compensation_failed {
                            "Hedge leg rejected; compensation void failed"
                        } else {
                            "Hedge leg rejected"
                        },
                        partial_events: all_events,
                    };
                }
            }
        }

        CrossMatchResult::Success {
            events: all_events,
            hedge_legs,
            worst_case_pnl,
        }
    }

    fn make_place_order_cmd(
        &mut self,
        market_id: MarketId,
        runner_id: RunnerId,
        side: Side,
        price: OddsX10000,
        stake: Money,
    ) -> Command {
        let correlation_id = crate::types::CorrelationId(self.next_correlation_id.to_string());
        self.next_correlation_id += 1;

        Command {
            correlation_id: Some(correlation_id),
            metadata: None,
            market_id,
            kind: CommandKind::PlaceOrder {
                runner_id,
                account_id: self.config.house_account.clone(),
                client_order_id: None,
                side,
                odds: price,
                stake,
                persistence: Persistence::Lapse,
                time_in_force: TimeInForce::FillOrKill { min_fill: None },
            },
        }
    }

    fn append_compensation_void(
        &mut self,
        book: &mut Book,
        market_id: MarketId,
        from_matched_at_inclusive: DateTime,
        all_events: &mut Vec<BookEventEnvelope>,
    ) -> Result<(), ()> {
        let correlation_id = crate::types::CorrelationId(self.next_correlation_id.to_string());
        let void_cmd = Command {
            correlation_id: Some(correlation_id),
            metadata: None,
            market_id,
            kind: CommandKind::VoidTradesFromTime {
                from_matched_at_inclusive,
                reason: "Cross-match hedge leg failed".to_string(),
            },
        };
        self.next_correlation_id += 1;
        let (void_events, _) = book.handle(&void_cmd).map_err(|_| ())?;
        book.apply_all_events(&void_events);
        all_events.extend(void_events);
        Ok(())
    }
}

/// Extract earliest matched-trade timestamp from book events.
fn extract_first_trade_match_ts(events: &[BookEventEnvelope]) -> Option<DateTime> {
    events.iter().find_map(|env| {
        matches!(&env.event, BookEvent::TradeMatched { .. }).then_some(env.timestamp)
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::book::protocol::command::{Command, CommandKind};
    use crate::book::{BookEvent, RunnerResult};
    use crate::types::{CorrelationId, RunnerId, unix_epoch};

    fn exec(book: &mut Book, cmd: Command) -> Vec<BookEventEnvelope> {
        let (events, _) = book.handle(&cmd).expect("command should succeed");
        book.apply_all_events(&events);
        events
    }

    #[test]
    fn append_compensation_void_appends_void_event() {
        let market_id = MarketId(7001);
        let mut engine = CrossMatchEngine::new(CrossMatchConfig::default());
        let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);
        let mut events = Vec::new();

        let res = engine.append_compensation_void(&mut book, market_id, unix_epoch(), &mut events);
        assert!(res.is_ok());
        assert!(events.iter().any(|e| {
            matches!(
                e.event,
                BookEvent::VoidTradesFromTime {
                    reason: ref r,
                    ..
                } if r == "Cross-match hedge leg failed"
            )
        }));
    }

    #[test]
    fn append_compensation_void_returns_error_when_void_rejected() {
        let market_id = MarketId(7002);
        let mut engine = CrossMatchEngine::new(CrossMatchConfig::default());
        let mut book = Book::new_multi_runner(market_id, [RunnerId(1), RunnerId(2), RunnerId(3)]);

        let _ = exec(
            &mut book,
            Command {
                correlation_id: Some(CorrelationId(1.to_string())),
                metadata: None,
                market_id,
                kind: CommandKind::CloseMarket {
                    batch_max_events: crate::config::DEFAULT_SET_MARKET_STATE_BATCH,
                },
            },
        );
        let _ = exec(
            &mut book,
            Command {
                correlation_id: Some(CorrelationId(2.to_string())),
                metadata: None,
                market_id,
                kind: CommandKind::SettleMarket {
                    runner_results: vec![
                        (RunnerId(1), RunnerResult::Winner),
                        (RunnerId(2), RunnerResult::Loser),
                        (RunnerId(3), RunnerResult::Loser),
                    ],
                    dead_heat_divisor: None,
                },
            },
        );

        let mut events = Vec::new();
        let res = engine.append_compensation_void(&mut book, market_id, unix_epoch(), &mut events);
        assert!(res.is_err(), "void should be rejected on terminal market");
        assert!(events.is_empty());
    }
}
