use pyo3::prelude::*;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};

// ---------------------------------------------------------------------------
// Enums
// ---------------------------------------------------------------------------

/// Side of a prediction market outcome.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Side {
    Yes,
    No,
}

#[pymethods]
impl Side {
    fn __repr__(&self) -> &'static str {
        match self {
            Side::Yes => "Side.Yes",
            Side::No => "Side.No",
        }
    }
    fn __str__(&self) -> &'static str {
        match self {
            Side::Yes => "YES",
            Side::No => "NO",
        }
    }
}

/// Buy or Sell direction.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderSide {
    Buy,
    Sell,
}

#[pymethods]
impl OrderSide {
    fn __repr__(&self) -> &'static str {
        match self {
            OrderSide::Buy => "OrderSide.Buy",
            OrderSide::Sell => "OrderSide.Sell",
        }
    }
}

/// Order type.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderType {
    Limit,
    Market,
}

/// Time in force.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TimeInForce {
    GTC,
    GTD,
    FOK,
    FAK,
}

/// Order status.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum OrderStatus {
    New,
    Submitted,
    Accepted,
    PartiallyFilled,
    Filled,
    Canceled,
    Rejected,
}

/// Alert level for risk events.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AlertLevel {
    Info,
    Warning,
    Critical,
}

// ---------------------------------------------------------------------------
// Market
// ---------------------------------------------------------------------------

/// A prediction market.
#[pyclass]
#[derive(Debug, Clone)]
pub struct Market {
    #[pyo3(get, set)]
    pub id: String,
    #[pyo3(get, set)]
    pub name: String,
    #[pyo3(get, set)]
    pub slug: String,
    #[pyo3(get, set)]
    pub exchange: String,
    #[pyo3(get, set)]
    pub expiry: Option<String>,
    #[pyo3(get, set)]
    pub active: bool,
    #[pyo3(get, set)]
    pub yes_token_id: Option<String>,
    #[pyo3(get, set)]
    pub no_token_id: Option<String>,
    #[pyo3(get, set)]
    pub condition_id: Option<String>,
    #[pyo3(get, set)]
    pub neg_risk: bool,
    #[pyo3(get, set)]
    pub ticker: Option<String>,
    /// Event ID if this market is an outcome in a multi-outcome event.
    #[pyo3(get, set)]
    pub event_id: Option<String>,
    /// Outcome name within the event (e.g., "Trump").
    #[pyo3(get, set)]
    pub outcome_name: Option<String>,
}

#[pymethods]
impl Market {
    #[new]
    #[pyo3(signature = (id, name="", slug="", exchange="paper", expiry=None, active=true,
                        yes_token_id=None, no_token_id=None, condition_id=None,
                        neg_risk=false, ticker=None, event_id=None, outcome_name=None))]
    fn new(
        id: String,
        name: &str,
        slug: &str,
        exchange: &str,
        expiry: Option<String>,
        active: bool,
        yes_token_id: Option<String>,
        no_token_id: Option<String>,
        condition_id: Option<String>,
        neg_risk: bool,
        ticker: Option<String>,
        event_id: Option<String>,
        outcome_name: Option<String>,
    ) -> Self {
        Self {
            id,
            name: name.to_string(),
            slug: slug.to_string(),
            exchange: exchange.to_string(),
            expiry,
            active,
            yes_token_id,
            no_token_id,
            condition_id,
            neg_risk,
            ticker,
            event_id,
            outcome_name,
        }
    }

    fn token_id(&self, side: Side) -> Option<String> {
        match side {
            Side::Yes => self.yes_token_id.clone(),
            Side::No => self.no_token_id.clone(),
        }
    }

    fn __repr__(&self) -> String {
        format!(
            "Market(id='{}', name='{}', exchange='{}')",
            self.id, self.name, self.exchange
        )
    }
}

// ---------------------------------------------------------------------------
// Outcome (multi-outcome event support)
// ---------------------------------------------------------------------------

/// One named outcome in a multi-outcome event. Each outcome is a binary contract.
#[pyclass]
#[derive(Debug, Clone)]
pub struct Outcome {
    #[pyo3(get, set)]
    pub name: String,
    #[pyo3(get, set)]
    pub market_id: String,
    #[pyo3(get, set)]
    pub yes_token_id: Option<String>,
    #[pyo3(get, set)]
    pub no_token_id: Option<String>,
    #[pyo3(get, set)]
    pub yes_price: f64,
}

#[pymethods]
impl Outcome {
    #[new]
    #[pyo3(signature = (name, market_id, yes_token_id=None, no_token_id=None, yes_price=0.0))]
    fn new(
        name: String,
        market_id: String,
        yes_token_id: Option<String>,
        no_token_id: Option<String>,
        yes_price: f64,
    ) -> Self {
        Self {
            name,
            market_id,
            yes_token_id,
            no_token_id,
            yes_price: yes_price.clamp(0.0, 1.0),
        }
    }

    fn token_id(&self, side: Side) -> Option<String> {
        match side {
            Side::Yes => self.yes_token_id.clone(),
            Side::No => self.no_token_id.clone(),
        }
    }

    fn __repr__(&self) -> String {
        format!(
            "Outcome(name='{}', market_id='{}', yes_price={:.4})",
            self.name, self.market_id, self.yes_price
        )
    }
}

// ---------------------------------------------------------------------------
// Event (multi-outcome event grouping)
// ---------------------------------------------------------------------------

/// Groups outcomes under a shared condition_id for multi-outcome events.
#[pyclass]
#[derive(Debug, Clone)]
pub struct Event {
    #[pyo3(get, set)]
    pub id: String,
    #[pyo3(get, set)]
    pub name: String,
    #[pyo3(get, set)]
    pub condition_id: Option<String>,
    #[pyo3(get, set)]
    pub neg_risk: bool,
    #[pyo3(get, set)]
    pub exchange: String,
    #[pyo3(get, set)]
    pub outcomes: Vec<Outcome>,
    #[pyo3(get, set)]
    pub expiry: Option<String>,
}

#[pymethods]
impl Event {
    #[new]
    #[pyo3(signature = (id, name, outcomes, neg_risk=false, exchange="polymarket",
                        condition_id=None, expiry=None))]
    fn new(
        id: String,
        name: String,
        outcomes: Vec<Outcome>,
        neg_risk: bool,
        exchange: &str,
        condition_id: Option<String>,
        expiry: Option<String>,
    ) -> Self {
        Self {
            id,
            name,
            condition_id,
            neg_risk,
            exchange: exchange.to_string(),
            outcomes,
            expiry,
        }
    }

    fn outcome_count(&self) -> usize {
        self.outcomes.len()
    }

    fn outcome_names(&self) -> Vec<String> {
        self.outcomes.iter().map(|o| o.name.clone()).collect()
    }

    fn outcome_by_name(&self, name: &str) -> Option<Outcome> {
        self.outcomes.iter().find(|o| o.name == name).cloned()
    }

    /// Convert each outcome into a standard Market with event_id and outcome_name set.
    fn to_markets(&self) -> Vec<Market> {
        self.outcomes
            .iter()
            .map(|o| Market {
                id: o.market_id.clone(),
                name: format!("{} - {}", self.name, o.name),
                slug: o.market_id.clone(),
                exchange: self.exchange.clone(),
                expiry: self.expiry.clone(),
                active: true,
                yes_token_id: o.yes_token_id.clone(),
                no_token_id: o.no_token_id.clone(),
                condition_id: self.condition_id.clone(),
                neg_risk: self.neg_risk,
                ticker: None,
                event_id: Some(self.id.clone()),
                outcome_name: Some(o.name.clone()),
            })
            .collect()
    }

    fn __repr__(&self) -> String {
        format!(
            "Event(id='{}', name='{}', outcomes={}, exchange='{}')",
            self.id,
            self.name,
            self.outcomes.len(),
            self.exchange
        )
    }
}

// ---------------------------------------------------------------------------
// Quote
// ---------------------------------------------------------------------------

/// A quote (bid + ask at a size).
#[pyclass]
#[derive(Debug, Clone)]
pub struct Quote {
    #[pyo3(get)]
    pub bid: f64,
    #[pyo3(get)]
    pub ask: f64,
    #[pyo3(get)]
    pub size: f64,
}

#[pymethods]
impl Quote {
    #[new]
    fn new(bid: f64, ask: f64, size: f64) -> PyResult<Self> {
        if !bid.is_finite() || !ask.is_finite() || !size.is_finite() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "Quote values must be finite",
            ));
        }
        if bid < 0.0 || ask < 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "Quote bid and ask must be non-negative",
            ));
        }
        if size <= 0.0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "Quote size must be positive",
            ));
        }
        if bid > ask {
            return Err(pyo3::exceptions::PyValueError::new_err(
                format!("Quote bid ({}) must not exceed ask ({})", bid, ask),
            ));
        }
        Ok(Self { bid, ask, size })
    }

    fn spread(&self) -> f64 {
        self.ask - self.bid
    }

    fn mid(&self) -> f64 {
        (self.bid + self.ask) / 2.0
    }

    fn __repr__(&self) -> String {
        format!(
            "Quote(bid={:.4}, ask={:.4}, size={:.1})",
            self.bid, self.ask, self.size
        )
    }
}

// ---------------------------------------------------------------------------
// RiskConfig
// ---------------------------------------------------------------------------

/// Risk configuration for the engine.
#[pyclass]
#[derive(Debug, Clone)]
pub struct RiskConfig {
    #[pyo3(get)]
    pub max_position_per_market: f64,
    #[pyo3(get)]
    pub max_portfolio_notional: f64,
    #[pyo3(get)]
    pub max_daily_drawdown_pct: f64,
    #[pyo3(get)]
    pub max_order_size: f64,
    #[pyo3(get)]
    pub rate_limit_sustained: u32,
    #[pyo3(get)]
    pub rate_limit_burst: u32,
    #[pyo3(get)]
    pub dedup_window_ms: u64,
    /// Max total position across all outcome markets in an event. None = disabled.
    #[pyo3(get)]
    pub max_position_per_event: Option<f64>,
}

#[pymethods]
impl RiskConfig {
    #[new]
    #[pyo3(signature = (max_position_per_market=100.0, max_portfolio_notional=1000.0,
                        max_daily_drawdown_pct=5.0, max_order_size=50.0,
                        rate_limit_sustained=50, rate_limit_burst=300,
                        dedup_window_ms=1000, max_position_per_event=None))]
    fn new(
        max_position_per_market: f64,
        max_portfolio_notional: f64,
        max_daily_drawdown_pct: f64,
        max_order_size: f64,
        rate_limit_sustained: u32,
        rate_limit_burst: u32,
        dedup_window_ms: u64,
        max_position_per_event: Option<f64>,
    ) -> PyResult<Self> {
        if max_position_per_market <= 0.0 || !max_position_per_market.is_finite() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "max_position_per_market must be positive and finite",
            ));
        }
        if max_portfolio_notional <= 0.0 || !max_portfolio_notional.is_finite() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "max_portfolio_notional must be positive and finite",
            ));
        }
        if max_daily_drawdown_pct <= 0.0 || !max_daily_drawdown_pct.is_finite() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "max_daily_drawdown_pct must be positive and finite",
            ));
        }
        if max_order_size <= 0.0 || !max_order_size.is_finite() {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "max_order_size must be positive and finite",
            ));
        }
        if rate_limit_sustained == 0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "rate_limit_sustained must be positive",
            ));
        }
        if rate_limit_burst == 0 {
            return Err(pyo3::exceptions::PyValueError::new_err(
                "rate_limit_burst must be positive",
            ));
        }
        Ok(Self {
            max_position_per_market,
            max_portfolio_notional,
            max_daily_drawdown_pct,
            max_order_size,
            rate_limit_sustained,
            rate_limit_burst,
            dedup_window_ms,
            max_position_per_event,
        })
    }
}

// ---------------------------------------------------------------------------
// OrderRequest
// ---------------------------------------------------------------------------

/// An order request (input to the engine).
#[pyclass]
#[derive(Debug, Clone)]
pub struct OrderRequest {
    #[pyo3(get)]
    pub market_id: String,
    #[pyo3(get)]
    pub side: Side,
    #[pyo3(get)]
    pub order_side: OrderSide,
    #[pyo3(get)]
    pub size: f64,
    #[pyo3(get)]
    pub price: f64,
    #[pyo3(get)]
    pub order_type: OrderType,
    #[pyo3(get)]
    pub time_in_force: TimeInForce,
    #[pyo3(get)]
    pub post_only: bool,
    /// Token ID for on-chain exchanges (e.g., Polymarket token_id).
    /// If set, this is used as the tokenID in signed orders instead of market_id.
    #[pyo3(get)]
    pub token_id: Option<String>,
    /// Whether this market uses neg-risk (determines contract address for Polymarket).
    #[pyo3(get)]
    pub neg_risk: bool,
}

#[pymethods]
impl OrderRequest {
    #[new]
    #[pyo3(signature = (market_id, side, order_side, size, price,
                        order_type=OrderType::Limit, time_in_force=TimeInForce::GTC,
                        post_only=true, token_id=None, neg_risk=false))]
    fn new(
        market_id: String,
        side: Side,
        order_side: OrderSide,
        size: f64,
        price: f64,
        order_type: OrderType,
        time_in_force: TimeInForce,
        post_only: bool,
        token_id: Option<String>,
        neg_risk: bool,
    ) -> Self {
        Self {
            market_id,
            side,
            order_side,
            size,
            price,
            order_type,
            time_in_force,
            post_only,
            token_id,
            neg_risk,
        }
    }

    fn __repr__(&self) -> String {
        format!(
            "OrderRequest({:?} {:?} {} x {:.1} @ {:.4})",
            self.side, self.order_side, self.market_id, self.size, self.price
        )
    }
}

impl OrderRequest {
    pub fn dedup_hash(&self) -> u64 {
        let mut hasher = DefaultHasher::new();
        self.market_id.hash(&mut hasher);
        self.side.hash(&mut hasher);
        self.order_side.hash(&mut hasher);
        self.size.to_bits().hash(&mut hasher);
        self.price.to_bits().hash(&mut hasher);
        hasher.finish()
    }
}

// ---------------------------------------------------------------------------
// Order
// ---------------------------------------------------------------------------

/// An order with current state.
#[pyclass]
#[derive(Debug, Clone)]
pub struct Order {
    #[pyo3(get)]
    pub id: String,
    #[pyo3(get)]
    pub market_id: String,
    #[pyo3(get)]
    pub side: Side,
    #[pyo3(get)]
    pub order_side: OrderSide,
    #[pyo3(get)]
    pub price: f64,
    #[pyo3(get)]
    pub size: f64,
    #[pyo3(get)]
    pub filled_size: f64,
    #[pyo3(get)]
    pub remaining_size: f64,
    #[pyo3(get)]
    pub order_type: OrderType,
    #[pyo3(get)]
    pub time_in_force: TimeInForce,
    #[pyo3(get)]
    pub status: OrderStatus,
    #[pyo3(get)]
    pub created_at: f64,
    #[pyo3(get)]
    pub status_reason: Option<String>,
    /// Exchange this order was submitted to (e.g., "polymarket", "kalshi", "paper").
    #[pyo3(get)]
    pub exchange: String,
    /// Number of times this order has been amended.
    #[pyo3(get)]
    pub amendment_count: u32,
    /// Token ID for on-chain exchanges (preserved for amendment).
    #[pyo3(get)]
    pub token_id: Option<String>,
    /// Whether this market uses neg-risk (preserved for amendment).
    #[pyo3(get)]
    pub neg_risk: bool,
}

#[pymethods]
impl Order {
    fn is_open(&self) -> bool {
        matches!(
            self.status,
            OrderStatus::New
                | OrderStatus::Submitted
                | OrderStatus::Accepted
                | OrderStatus::PartiallyFilled
        )
    }

    fn __repr__(&self) -> String {
        format!(
            "Order(id='{}', {:?} {:?} {:.1} @ {:.4}, type={:?}, tif={:?}, status={:?})",
            self.id, self.side, self.order_side, self.size, self.price,
            self.order_type, self.time_in_force, self.status
        )
    }
}

// ---------------------------------------------------------------------------
// Position
// ---------------------------------------------------------------------------

/// A position in a market.
#[pyclass]
#[derive(Debug, Clone)]
pub struct Position {
    #[pyo3(get)]
    pub market_id: String,
    #[pyo3(get)]
    pub side: Side,
    #[pyo3(get)]
    pub size: f64,
    #[pyo3(get)]
    pub avg_entry_price: f64,
    #[pyo3(get)]
    pub realized_pnl: f64,
    #[pyo3(get)]
    pub unrealized_pnl: f64,
    #[pyo3(get)]
    pub token_id: Option<String>,
    /// Exchange this position is on (e.g., "polymarket", "kalshi", "paper").
    #[pyo3(get)]
    pub exchange: String,
}

#[pymethods]
impl Position {
    fn total_pnl(&self) -> f64 {
        self.realized_pnl + self.unrealized_pnl
    }

    fn notional(&self) -> f64 {
        self.size * self.avg_entry_price
    }

    fn __repr__(&self) -> String {
        format!(
            "Position({} {:?} {:.1} @ {:.4}, pnl={:.2})",
            self.market_id,
            self.side,
            self.size,
            self.avg_entry_price,
            self.total_pnl()
        )
    }
}

// ---------------------------------------------------------------------------
// Fill
// ---------------------------------------------------------------------------

/// A fill event.
#[pyclass]
#[derive(Debug, Clone)]
pub struct Fill {
    #[pyo3(get)]
    pub fill_id: String,
    #[pyo3(get)]
    pub order_id: String,
    #[pyo3(get)]
    pub market_id: String,
    #[pyo3(get)]
    pub side: Side,
    #[pyo3(get)]
    pub order_side: OrderSide,
    #[pyo3(get)]
    pub price: f64,
    #[pyo3(get)]
    pub size: f64,
    #[pyo3(get)]
    pub fee: f64,
    #[pyo3(get)]
    pub timestamp: f64,
    #[pyo3(get)]
    pub token_id: Option<String>,
    /// Exchange this fill came from (e.g., "polymarket", "kalshi", "paper").
    #[pyo3(get)]
    pub exchange: String,
    /// Whether this fill was a maker (added liquidity) or taker (removed liquidity).
    #[pyo3(get)]
    pub is_maker: bool,
}

#[pymethods]
impl Fill {
    #[new]
    #[pyo3(signature = (fill_id, order_id, market_id, side, order_side, price, size,
                        fee=0.0, timestamp=0.0, token_id=None, exchange="", is_maker=false))]
    fn new(
        fill_id: String,
        order_id: String,
        market_id: String,
        side: Side,
        order_side: OrderSide,
        price: f64,
        size: f64,
        fee: f64,
        timestamp: f64,
        token_id: Option<String>,
        exchange: &str,
        is_maker: bool,
    ) -> Self {
        Self {
            fill_id,
            order_id,
            market_id,
            side,
            order_side,
            price,
            size,
            fee,
            timestamp,
            token_id,
            exchange: exchange.to_string(),
            is_maker,
        }
    }

    fn __repr__(&self) -> String {
        format!(
            "Fill({:?} {:?} {} {:.1} @ {:.4})",
            self.side, self.order_side, self.market_id, self.size, self.price
        )
    }
}

// ---------------------------------------------------------------------------
// EngineStatus
// ---------------------------------------------------------------------------

/// Engine status snapshot.
#[pyclass]
#[derive(Debug, Clone)]
pub struct EngineStatus {
    #[pyo3(get)]
    pub running: bool,
    #[pyo3(get)]
    pub kill_switch_active: bool,
    #[pyo3(get)]
    pub kill_switch_reason: Option<String>,
    #[pyo3(get)]
    pub open_orders: u32,
    #[pyo3(get)]
    pub active_positions: u32,
    #[pyo3(get)]
    pub total_realized_pnl: f64,
    #[pyo3(get)]
    pub total_unrealized_pnl: f64,
    #[pyo3(get)]
    pub daily_pnl: f64,
    #[pyo3(get)]
    pub uptime_secs: u64,
}

#[pymethods]
impl EngineStatus {
    fn total_pnl(&self) -> f64 {
        self.total_realized_pnl + self.total_unrealized_pnl
    }

    fn __repr__(&self) -> String {
        format!(
            "EngineStatus(running={}, orders={}, positions={}, pnl={:.2})",
            self.running,
            self.open_orders,
            self.active_positions,
            self.total_pnl()
        )
    }
}

// ---------------------------------------------------------------------------
// TriggerType
// ---------------------------------------------------------------------------

/// Type of contingent order trigger.
#[pyclass(eq, eq_int, frozen, hash)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TriggerType {
    StopLoss,
    TakeProfit,
}

#[pymethods]
impl TriggerType {
    fn __repr__(&self) -> &'static str {
        match self {
            TriggerType::StopLoss => "TriggerType.StopLoss",
            TriggerType::TakeProfit => "TriggerType.TakeProfit",
        }
    }
}

// ---------------------------------------------------------------------------
// ContingentOrder
// ---------------------------------------------------------------------------

/// A contingent order (stop-loss or take-profit) that triggers at a price level.
#[pyclass]
#[derive(Debug, Clone)]
pub struct ContingentOrder {
    #[pyo3(get)]
    pub id: String,
    #[pyo3(get)]
    pub trigger_type: TriggerType,
    #[pyo3(get)]
    pub market_id: String,
    #[pyo3(get)]
    pub side: Side,
    #[pyo3(get)]
    pub order_side: OrderSide,
    #[pyo3(get)]
    pub size: f64,
    #[pyo3(get)]
    pub trigger_price: f64,
    #[pyo3(get)]
    pub trigger_pnl: Option<f64>,
    #[pyo3(get)]
    pub linked_order_id: Option<String>,
    #[pyo3(get)]
    pub exchange: String,
    #[pyo3(get)]
    pub triggered: bool,
    #[pyo3(get)]
    pub child_order_id: Option<String>,
    #[pyo3(get)]
    pub token_id: Option<String>,
    #[pyo3(get)]
    pub neg_risk: bool,
}

#[pymethods]
impl ContingentOrder {
    fn __repr__(&self) -> String {
        format!(
            "ContingentOrder(id='{}', {:?} {} {:?} {:.1} @ trigger={:.4})",
            self.id, self.trigger_type, self.market_id, self.order_side, self.size, self.trigger_price
        )
    }
}
