use betex::{
    book::{BookMarketState, BookOrderState, TradeRole},
    engine::root::EngineRoot,
    prelude::*,
    snapshot::MarketSnapshot,
};
use serde::{Deserialize, Serialize};
use std::{
    collections::{HashMap, HashSet},
    path::Path as StdPath,
    sync::Arc,
};

type TradePairKey = (OrderId, OrderId);

// ==========================================================================
// Projection Snapshot
// ==========================================================================

#[derive(Clone, Default)]
pub(crate) struct ProjectionSnapshot {
    pub(crate) global_seq: u64,
    pub(crate) markets: Vec<MarketSnapshot>,
}

// ==========================================================================
// Live (Queryable) Projection State
// ==========================================================================

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub(crate) enum TerminalEventKind {
    ExecutionComplete,
    Cancelled,
    Lapsed,
    Voided,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct CompletedOrder {
    pub(crate) market_id: MarketId,
    pub(crate) order_id: OrderId,
    pub(crate) account_id: AccountId,
    pub(crate) runner_id: RunnerId,
    pub(crate) side: Side,
    pub(crate) odds: OddsX10000,
    pub(crate) stake: Money,
    pub(crate) matched: Money,
    pub(crate) terminal: TerminalEventKind,
    /// Optional reason (present for Cancelled/Lapsed/Voided).
    pub(crate) reason: Option<String>,
    /// When the terminal transition was observed by the projection.
    pub(crate) terminal_at: i64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LiveOrder {
    pub(crate) order_id: OrderId,
    pub(crate) account_id: AccountId,
    pub(crate) runner_id: RunnerId,
    pub(crate) side: Side,
    pub(crate) odds: OddsX10000,
    pub(crate) stake: Money,
    pub(crate) matched: Money,
    pub(crate) state: BookOrderState,
    pub(crate) created_at: i64,
    pub(crate) last_updated_at: i64,
}

impl LiveOrder {
    pub(crate) fn remaining(&self) -> Money {
        Money(self.stake.0.saturating_sub(self.matched.0).max(0))
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LiveTrade {
    pub(crate) maker_account_id: AccountId,
    pub(crate) maker_order_id: OrderId,
    pub(crate) maker_side: Side,
    pub(crate) taker_account_id: AccountId,
    pub(crate) taker_order_id: OrderId,
    pub(crate) taker_side: Side,
    pub(crate) runner_id: RunnerId,
    pub(crate) price: OddsX10000,
    pub(crate) stake: Money,
    pub(crate) matched_at: i64,
    pub(crate) voided: bool,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct PendingTradeView {
    pub(crate) order_id: OrderId,
    #[serde(default = "default_trade_role")]
    pub(crate) role: TradeRole,
    pub(crate) runner_id: RunnerId,
    pub(crate) side: Side,
    pub(crate) price: OddsX10000,
    pub(crate) stake: Money,
    pub(crate) matched_delta: Money,
    pub(crate) account_id: AccountId,
}

fn default_trade_role() -> TradeRole {
    TradeRole::Maker
}

fn default_market_state() -> BookMarketState {
    BookMarketState::Open
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MarketLiveState {
    #[serde(default = "default_market_state")]
    pub(crate) current_state: BookMarketState,
    pub(crate) orders: HashMap<OrderId, LiveOrder>,
    pub(crate) open_orders: HashSet<OrderId>,
    pub(crate) completed_orders: Vec<CompletedOrder>,
    pub(crate) trades: HashMap<TradePairKey, LiveTrade>,
    #[serde(default)]
    pub(crate) pending_trades: HashMap<TradePairKey, PendingTradeView>,
}

impl Default for MarketLiveState {
    fn default() -> Self {
        Self {
            current_state: default_market_state(),
            orders: HashMap::new(),
            open_orders: HashSet::new(),
            completed_orders: Vec::new(),
            trades: HashMap::new(),
            pending_trades: HashMap::new(),
        }
    }
}

#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub(crate) struct LiveState {
    pub(crate) global_seq: u64,
    pub(crate) markets: HashMap<MarketId, MarketLiveState>,
}

// ==========================================================================
// Market Info & Control Plane Types
// ==========================================================================

pub(crate) struct MarketIdAllocator {
    counter: Arc<std::sync::atomic::AtomicU64>,
}

impl MarketIdAllocator {
    pub(crate) fn new() -> Self {
        Self {
            counter: Arc::new(std::sync::atomic::AtomicU64::new(1)),
        }
    }

    pub(crate) fn next(&self) -> MarketId {
        MarketId(
            self.counter
                .fetch_add(1, std::sync::atomic::Ordering::Relaxed),
        )
    }

    pub(crate) fn counter(&self) -> Arc<std::sync::atomic::AtomicU64> {
        Arc::clone(&self.counter)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct MarketMeta {
    pub(crate) name: String,
    pub(crate) market_model: MarketModel,
    pub(crate) market_kind: MarketKind,
    pub(crate) runner_ids: Vec<RunnerId>,
    pub(crate) runner_labels: Vec<String>,
    pub(crate) created_at_ms: i64,
}

impl MarketLiveState {
    pub(crate) fn mark_terminal(
        &mut self,
        market_id: MarketId,
        ts_ms: i64,
        order_id: OrderId,
        terminal: TerminalEventKind,
        reason: Option<String>,
    ) {
        let Some(mut order) = self.orders.remove(&order_id) else {
            return;
        };

        order.state = match terminal {
            TerminalEventKind::ExecutionComplete => BookOrderState::ExecutionComplete,
            TerminalEventKind::Cancelled => BookOrderState::Cancelled,
            TerminalEventKind::Lapsed => BookOrderState::Lapsed,
            TerminalEventKind::Voided => BookOrderState::Voided,
        };
        order.last_updated_at = ts_ms;
        self.open_orders.remove(&order_id);

        self.completed_orders.push(CompletedOrder {
            market_id,
            order_id,
            account_id: order.account_id,
            runner_id: order.runner_id,
            side: order.side,
            odds: order.odds,
            stake: order.stake,
            matched: order.matched,
            terminal,
            reason,
            terminal_at: ts_ms,
        });
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ProjectionDiskSnapshot {
    pub(crate) market_ids: Vec<MarketId>,
    pub(crate) root: EngineRoot,
    pub(crate) live: LiveState,
    pub(crate) market_meta: HashMap<MarketId, MarketMeta>,
    pub(crate) market_id_counter: u64,
}

#[derive(Serialize)]
#[allow(dead_code)]
pub(crate) struct ProjectionDiskSnapshotRef<'a> {
    market_ids: &'a [MarketId],
    root: &'a EngineRoot,
    live: &'a LiveState,
    market_meta: &'a HashMap<MarketId, MarketMeta>,
    market_id_counter: u64,
}

impl ProjectionDiskSnapshot {
    pub(crate) fn global_seq(&self) -> u64 {
        self.root.version()
    }

    pub(crate) fn save_postcard_ref(
        path: &StdPath,
        market_ids: &[MarketId],
        root: &EngineRoot,
        live: &LiveState,
        market_meta: &HashMap<MarketId, MarketMeta>,
        market_id_counter: u64,
    ) -> anyhow::Result<()> {
        let tmp = path.with_extension("tmp");
        let f = std::fs::File::create(&tmp)?;
        let mut w = std::io::BufWriter::new(f);
        let data = postcard::to_allocvec(&ProjectionDiskSnapshotRef {
            market_ids,
            root,
            live,
            market_meta,
            market_id_counter,
        })?;
        use std::io::Write;
        w.write_all(&data)?;
        w.flush()?;
        std::fs::rename(tmp, path)?;
        Ok(())
    }

    pub(crate) fn load_postcard(path: &StdPath) -> anyhow::Result<Option<Self>> {
        if !path.exists() {
            return Ok(None);
        }
        let data = std::fs::read(path)?;
        let snapshot: Self = postcard::from_bytes(&data)?;
        Ok(Some(snapshot))
    }
}
