//! Single-market, deterministic order book module.
//!
//! Design goals (Betfair parity):
//! - **Single market**: one `Book` instance == one `market_id` source of truth.
//! - **Monotonic event stream**: every mutation emits events wrapped in `BookEventEnvelope { seq, ... }`.
//! - **Deterministic**: given the same initial book + same ordered commands, the emitted events are identical.
//! - **Matchability gating**: no matching occurs unless market state is matchable.
//! - **Forward-only execution state**: books update active orders and emit events; settlement/void policy
//!   is delegated to downstream systems.
//! - **Betfair tick ladder**: prices must be on the 350-price tick ladder (1.01 to 1000).
//! - **Time-in-force**: GTC (default) and Fill-or-Kill with optional minFillSize.
//!
//! ## Book Variants
//!
//! - **TwoRunnerBook**: For 2-runner markets (e.g., tennis, head-to-head). Uses implied matching
//!   where BACK A @ odds `a` ≡ LAY B @ odds `a/(a-1)`. No external hedging needed.
//! - **MultiRunnerBook**: For 3+ runner markets. Direct matching only; cross-matching is handled
//!   by an external `CrossMatchEngine`.
//!
//! Non-goals (by design, to keep the book "pure"):
//! - No wallet/risk/auth; the gateway is assumed to pre-authorize commands.
//! - No in-play delay queueing (that lives in front of the book).

mod binary_yes;
pub mod common;
mod error;
mod multi_runner;
pub mod protocol;
mod two_runner;

// Re-export common protocol types at the module level.
use crate::types::*;
pub use binary_yes::BinaryYesBook;
pub use common::*;
pub use error::{BookError, BookErrorDetail, RequestedBookState};
pub use multi_runner::MultiRunnerBook;
use protocol::command::{Command, CommandKind};
pub use protocol::command::{Persistence, TimeInForce};
pub use two_runner::TwoRunnerBook;

// ============================================================================
// Book Enum
// ============================================================================

/// A single-market order book that automatically selects the optimal implementation.
///
/// - For 2-runner markets: uses `TwoRunnerBook` with implied matching.
/// - For 3+ runner markets: uses `MultiRunnerBook` with direct matching only.
///
/// This enum provides a unified interface while allowing each variant to have
/// specialized matching logic.
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum Book {
    TwoRunner(Box<TwoRunnerBook>),
    MultiRunner(Box<MultiRunnerBook>),
    BinaryYes(Box<BinaryYesBook>),
}

impl Book {
    const DEFAULT_ORDER_STORE_CAPACITY: usize = 20_000;

    fn default_phase_for_kind(market_kind: MarketKind) -> MarketPhase {
        if market_kind == MarketKind::LiveOnly {
            MarketPhase::Live
        } else {
            MarketPhase::Pre
        }
    }

    /// Create a new book for a market with the given runners.
    ///
    /// Automatically selects the appropriate implementation based on runner count:
    /// - 2 runners: `TwoRunnerBook` with implied matching
    /// - 3+ runners: `MultiRunnerBook` with direct matching only
    ///
    /// # Panics
    /// Panics if fewer than 2 runners are provided.
    pub fn new(market_id: MarketId, runner_ids: impl IntoIterator<Item = RunnerId>) -> Self {
        Self::new_with_kind(market_id, MarketKind::InPlayCapable, runner_ids)
    }

    pub fn new_with_kind(
        market_id: MarketId,
        market_kind: MarketKind,
        runner_ids: impl IntoIterator<Item = RunnerId>,
    ) -> Self {
        Self::new_with_kind_and_phase(
            market_id,
            market_kind,
            Self::default_phase_for_kind(market_kind),
            runner_ids,
        )
    }

    pub fn new_with_kind_and_phase(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
        runner_ids: impl IntoIterator<Item = RunnerId>,
    ) -> Self {
        let runners: Vec<RunnerId> = runner_ids.into_iter().collect();
        match runners.len() {
            0 | 1 => panic!("Book requires at least 2 runners"),
            2 => Book::TwoRunner(Box::new(TwoRunnerBook::new_with_capacity(
                market_id,
                market_kind,
                market_phase,
                runners[0],
                runners[1],
                Self::DEFAULT_ORDER_STORE_CAPACITY,
            ))),
            _ => Book::MultiRunner(Box::new(MultiRunnerBook::new_with_capacity(
                market_id,
                market_kind,
                market_phase,
                runners,
                Self::DEFAULT_ORDER_STORE_CAPACITY,
            ))),
        }
    }

    /// Create a new empty book suitable for engine integration.
    ///
    /// This always creates a `MultiRunnerBook` since the runner count may not be
    /// known at bootstrap time, but orders on unknown runners are rejected until
    /// runners are explicitly configured.
    pub fn new_engine(market_id: MarketId) -> Self {
        Self::new_engine_with_kind(market_id, MarketKind::InPlayCapable)
    }

    pub fn new_engine_with_kind(market_id: MarketId, market_kind: MarketKind) -> Self {
        Self::new_engine_with_kind_and_phase(
            market_id,
            market_kind,
            Self::default_phase_for_kind(market_kind),
        )
    }

    pub fn new_engine_with_kind_and_phase(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
    ) -> Self {
        Book::MultiRunner(Box::new(MultiRunnerBook::new_engine_with_capacity(
            market_id,
            market_kind,
            market_phase,
            Self::DEFAULT_ORDER_STORE_CAPACITY,
        )))
    }

    pub fn new_engine_with_capacity(market_id: MarketId, order_store_capacity: usize) -> Self {
        Self::new_engine_with_kind_and_capacity(
            market_id,
            MarketKind::InPlayCapable,
            MarketPhase::Pre,
            order_store_capacity,
        )
    }

    pub fn new_engine_with_kind_and_capacity(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
        order_store_capacity: usize,
    ) -> Self {
        Book::MultiRunner(Box::new(MultiRunnerBook::new_engine_with_capacity(
            market_id,
            market_kind,
            market_phase,
            order_store_capacity,
        )))
    }

    /// Create a two-runner book explicitly.
    pub fn new_two_runner(market_id: MarketId, runner_a: RunnerId, runner_b: RunnerId) -> Self {
        Self::new_two_runner_with_kind(market_id, MarketKind::InPlayCapable, runner_a, runner_b)
    }

    pub fn new_two_runner_with_kind(
        market_id: MarketId,
        market_kind: MarketKind,
        runner_a: RunnerId,
        runner_b: RunnerId,
    ) -> Self {
        Self::new_two_runner_with_kind_and_phase(
            market_id,
            market_kind,
            Self::default_phase_for_kind(market_kind),
            runner_a,
            runner_b,
        )
    }

    pub fn new_two_runner_with_kind_and_phase(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
        runner_a: RunnerId,
        runner_b: RunnerId,
    ) -> Self {
        Book::TwoRunner(Box::new(TwoRunnerBook::new_with_capacity(
            market_id,
            market_kind,
            market_phase,
            runner_a,
            runner_b,
            Self::DEFAULT_ORDER_STORE_CAPACITY,
        )))
    }

    pub fn new_two_runner_with_capacity(
        market_id: MarketId,
        runner_a: RunnerId,
        runner_b: RunnerId,
        order_store_capacity: usize,
    ) -> Self {
        Self::new_two_runner_with_kind_and_capacity(
            market_id,
            MarketKind::InPlayCapable,
            MarketPhase::Pre,
            runner_a,
            runner_b,
            order_store_capacity,
        )
    }

    pub fn new_two_runner_with_kind_and_capacity(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
        runner_a: RunnerId,
        runner_b: RunnerId,
        order_store_capacity: usize,
    ) -> Self {
        Book::TwoRunner(Box::new(TwoRunnerBook::new_with_capacity(
            market_id,
            market_kind,
            market_phase,
            runner_a,
            runner_b,
            order_store_capacity,
        )))
    }

    /// Create a multi-runner book explicitly.
    pub fn new_multi_runner(
        market_id: MarketId,
        runner_ids: impl IntoIterator<Item = RunnerId>,
    ) -> Self {
        Self::new_multi_runner_with_kind(market_id, MarketKind::InPlayCapable, runner_ids)
    }

    pub fn new_multi_runner_with_kind(
        market_id: MarketId,
        market_kind: MarketKind,
        runner_ids: impl IntoIterator<Item = RunnerId>,
    ) -> Self {
        Self::new_multi_runner_with_kind_and_phase(
            market_id,
            market_kind,
            Self::default_phase_for_kind(market_kind),
            runner_ids,
        )
    }

    pub fn new_multi_runner_with_kind_and_phase(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
        runner_ids: impl IntoIterator<Item = RunnerId>,
    ) -> Self {
        Book::MultiRunner(Box::new(MultiRunnerBook::new_with_capacity(
            market_id,
            market_kind,
            market_phase,
            runner_ids,
            Self::DEFAULT_ORDER_STORE_CAPACITY,
        )))
    }

    pub fn new_multi_runner_with_capacity(
        market_id: MarketId,
        runner_ids: impl IntoIterator<Item = RunnerId>,
        order_store_capacity: usize,
    ) -> Self {
        Self::new_multi_runner_with_kind_and_capacity(
            market_id,
            MarketKind::InPlayCapable,
            MarketPhase::Pre,
            runner_ids,
            order_store_capacity,
        )
    }

    pub fn new_multi_runner_with_kind_and_capacity(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
        runner_ids: impl IntoIterator<Item = RunnerId>,
        order_store_capacity: usize,
    ) -> Self {
        Book::MultiRunner(Box::new(MultiRunnerBook::new_with_capacity(
            market_id,
            market_kind,
            market_phase,
            runner_ids,
            order_store_capacity,
        )))
    }

    /// Create a canonical YES-only prediction market book.
    pub fn new_binary_yes_with_kind_and_capacity(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
        yes_runner_id: RunnerId,
        no_runner_id: RunnerId,
        max_price_ticks: u16,
        order_store_capacity: usize,
    ) -> Self {
        Book::BinaryYes(Box::new(BinaryYesBook::new_with_capacity(
            market_id,
            market_kind,
            market_phase,
            yes_runner_id,
            no_runner_id,
            max_price_ticks,
            order_store_capacity,
        )))
    }

    pub fn new_binary_yes_with_kind(
        market_id: MarketId,
        market_kind: MarketKind,
        yes_runner_id: RunnerId,
        no_runner_id: RunnerId,
        max_price_ticks: u16,
    ) -> Self {
        Self::new_binary_yes_with_kind_and_phase(
            market_id,
            market_kind,
            Self::default_phase_for_kind(market_kind),
            yes_runner_id,
            no_runner_id,
            max_price_ticks,
        )
    }

    pub fn new_binary_yes_with_kind_and_phase(
        market_id: MarketId,
        market_kind: MarketKind,
        market_phase: MarketPhase,
        yes_runner_id: RunnerId,
        no_runner_id: RunnerId,
        max_price_ticks: u16,
    ) -> Self {
        Self::new_binary_yes_with_kind_and_capacity(
            market_id,
            market_kind,
            market_phase,
            yes_runner_id,
            no_runner_id,
            max_price_ticks,
            Self::DEFAULT_ORDER_STORE_CAPACITY,
        )
    }

    pub fn new_binary_yes(
        market_id: MarketId,
        yes_runner_id: RunnerId,
        no_runner_id: RunnerId,
        max_price_ticks: u16,
    ) -> Self {
        Self::new_binary_yes_with_kind(
            market_id,
            MarketKind::InPlayCapable,
            yes_runner_id,
            no_runner_id,
            max_price_ticks,
        )
    }

    // ========================================================================
    // Accessors
    // ========================================================================

    pub fn set_market_name(&mut self, name: &str) {
        match self {
            Book::TwoRunner(b) => b.market_name = name.to_owned(),
            Book::MultiRunner(b) => b.market_name = name.to_owned(),
            Book::BinaryYes(b) => b.market_name = name.to_owned(),
        }
    }

    pub fn set_runner_labels(&mut self, runner_ids: &[RunnerId], runner_labels: &[String]) {
        match self {
            Book::TwoRunner(b) => b.set_runner_labels(runner_ids, runner_labels),
            Book::MultiRunner(b) => b.set_runner_labels(runner_ids, runner_labels),
            Book::BinaryYes(b) => b.set_runner_labels(runner_ids, runner_labels),
        }
    }

    pub fn market_id(&self) -> MarketId {
        match self {
            Book::TwoRunner(b) => b.market_id(),
            Book::MultiRunner(b) => b.market_id(),
            Book::BinaryYes(b) => b.market_id(),
        }
    }

    pub fn market_model(&self) -> MarketModel {
        match self {
            Book::TwoRunner(_) | Book::MultiRunner(_) => MarketModel::ExchangeOdds,
            Book::BinaryYes(b) => MarketModel::BinaryYes {
                max_price_ticks: b.max_price_ticks(),
            },
        }
    }

    pub fn binary_depth(&self, depth: usize) -> Option<BinaryDepth> {
        match self {
            Book::BinaryYes(b) => Some(b.depth(depth)),
            _ => None,
        }
    }

    pub fn market_state(&self) -> BookMarketState {
        match self {
            Book::TwoRunner(b) => b.market_state(),
            Book::MultiRunner(b) => b.market_state(),
            Book::BinaryYes(b) => b.market_state(),
        }
    }

    pub fn market_phase(&self) -> MarketPhase {
        match self {
            Book::TwoRunner(b) => b.market_phase(),
            Book::MultiRunner(b) => b.market_phase(),
            Book::BinaryYes(b) => b.market_phase(),
        }
    }

    pub fn is_halted(&self) -> bool {
        match self {
            Book::TwoRunner(b) => b.is_halted(),
            Book::MultiRunner(b) => b.is_halted(),
            Book::BinaryYes(b) => b.is_halted(),
        }
    }

    pub fn get_order(&self, order_id: OrderId) -> Option<&BookOrder> {
        match self {
            Book::TwoRunner(b) => b.get_order(order_id),
            Book::MultiRunner(b) => b.get_order(order_id),
            Book::BinaryYes(_) => None,
        }
    }

    pub fn is_resting(&self, order_id: OrderId) -> bool {
        match self {
            Book::TwoRunner(b) => b.is_resting(order_id),
            Book::MultiRunner(b) => b.is_resting(order_id),
            Book::BinaryYes(b) => b.is_resting(order_id),
        }
    }

    pub fn active_order_count(&self) -> usize {
        // Note: this is O(n) over all orders and intended for diagnostics/UI.
        match self {
            Book::TwoRunner(b) => b.active_order_count(),
            Book::MultiRunner(b) => b.active_order_count(),
            Book::BinaryYes(b) => b.active_order_count(),
        }
    }

    pub fn close_process_state(&self) -> Option<CloseProcessState> {
        match self {
            Book::TwoRunner(b) => b.close_process_state(),
            Book::MultiRunner(b) => b.close_process_state(),
            Book::BinaryYes(b) => b.close_process_state(),
        }
    }

    pub fn cancel_process_state(&self) -> Option<&CancelProcessState> {
        match self {
            Book::TwoRunner(b) => b.cancel_process_state(),
            Book::MultiRunner(b) => b.cancel_process_state(),
            Book::BinaryYes(b) => b.cancel_process_state(),
        }
    }

    pub fn batch_process_state(&self) -> Option<&BatchProcessState> {
        match self {
            Book::TwoRunner(b) => b.batch_process_state(),
            Book::MultiRunner(b) => b.batch_process_state(),
            Book::BinaryYes(b) => b.batch_process_state(),
        }
    }

    /// Returns an iterator over all runner IDs in this market.
    pub fn runners(&self) -> Box<dyn Iterator<Item = RunnerId> + '_> {
        match self {
            Book::TwoRunner(b) => Box::new(b.runners()),
            Book::MultiRunner(b) => Box::new(b.runners()),
            Book::BinaryYes(b) => Box::new(b.runners()),
        }
    }

    // ========================================================================
    // Market Depth Queries
    // ========================================================================

    pub fn runner_prices(&self, runner_id: RunnerId, depth: usize) -> RunnerPrices {
        match self {
            Book::TwoRunner(b) => b.runner_prices(runner_id, depth),
            Book::MultiRunner(b) => b.runner_prices(runner_id, depth),
            Book::BinaryYes(b) => b.runner_prices(runner_id, depth),
        }
    }

    pub fn best_back_price(&self, runner_id: RunnerId) -> Option<PriceSize> {
        match self {
            Book::TwoRunner(b) => b.best_back_price(runner_id),
            Book::MultiRunner(b) => b.best_back_price(runner_id),
            Book::BinaryYes(b) => b.best_back_price(runner_id),
        }
    }

    pub fn best_lay_price(&self, runner_id: RunnerId) -> Option<PriceSize> {
        match self {
            Book::TwoRunner(b) => b.best_lay_price(runner_id),
            Book::MultiRunner(b) => b.best_lay_price(runner_id),
            Book::BinaryYes(b) => b.best_lay_price(runner_id),
        }
    }

    pub fn runner_matched_volume(&self, runner_id: RunnerId) -> Money {
        match self {
            Book::TwoRunner(b) => b.runner_matched_volume(runner_id),
            Book::MultiRunner(b) => b.runner_matched_volume(runner_id),
            Book::BinaryYes(b) => b.runner_matched_volume(runner_id),
        }
    }

    pub fn runner_label(&self, runner_id: RunnerId) -> &str {
        match self {
            Book::TwoRunner(b) => b.runner_label(runner_id),
            Book::MultiRunner(b) => b.runner_label(runner_id),
            Book::BinaryYes(b) => b.runner_label(runner_id),
        }
    }

    pub fn total_matched(&self) -> Money {
        match self {
            Book::TwoRunner(b) => b.total_matched(),
            Book::MultiRunner(b) => b.total_matched(),
            Book::BinaryYes(b) => b.total_matched(),
        }
    }

    // ========================================================================
    // Command Processing
    // ========================================================================

    pub fn handle(
        &mut self,
        cmd: &Command,
    ) -> Result<(Vec<BookEventEnvelope>, CommandResponse), BookError> {
        let (mut events, resp) = match self {
            Book::TwoRunner(b) => {
                let (events, resp) = b.handle_command(cmd)?;
                (events.into_vec(), resp)
            }
            Book::MultiRunner(b) => b.handle_command(cmd)?,
            Book::BinaryYes(b) => b.handle_command(cmd)?,
        };
        Self::attach_metadata_to_events(cmd, &mut events, &cmd.metadata);
        Ok((events, resp))
    }

    fn attach_metadata_to_events(
        cmd: &Command,
        events: &mut [BookEventEnvelope],
        metadata: &Option<serde_json::Value>,
    ) {
        if matches!(
            cmd.kind,
            CommandKind::BatchCancelOrders { .. }
                | CommandKind::ContinueBatchCancelOrders
                | CommandKind::ContinueLapseOrders
                | CommandKind::ContinueVoidOrders
        ) {
            return;
        }

        for event in events {
            event.metadata = metadata.clone();
        }
    }

    pub fn apply_event(&mut self, env: &BookEventEnvelope) {
        debug_assert_eq!(
            env.market_id,
            self.market_id(),
            "event market_id mismatch: expected {:?}, got {:?}",
            self.market_id(),
            env.market_id
        );
        match self {
            Book::TwoRunner(b) => b.apply_event(env),
            Book::MultiRunner(b) => b.apply_event(env),
            Book::BinaryYes(b) => b.apply_event(env),
        }
    }

    pub fn apply_all_events(&mut self, envs: &[BookEventEnvelope]) {
        for env in envs {
            self.apply_event(env);
        }
    }
}
