use crate::types::{Fill, OrderSide, Position, Side};
use dashmap::DashMap;
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicU32, Ordering};
use std::sync::RwLock;
use tracing::{info, warn};

type PositionKey = (String, Side);

/// Real-time position tracker with P&L computation.
///
/// Maintains cached aggregates updated incrementally on fills and mark price
/// updates, so that status() and snapshot_exposure() are O(1).
pub struct PositionTracker {
    positions: DashMap<PositionKey, Position>,
    cached_realized_pnl: RwLock<f64>,
    cached_unrealized_pnl: RwLock<f64>,
    cached_total_notional: RwLock<f64>,
    cached_active_count: AtomicU32,
}

impl PositionTracker {
    pub fn new() -> Self {
        Self {
            positions: DashMap::new(),
            cached_realized_pnl: RwLock::new(0.0),
            cached_unrealized_pnl: RwLock::new(0.0),
            cached_total_notional: RwLock::new(0.0),
            cached_active_count: AtomicU32::new(0),
        }
    }

    /// Recompute all cached aggregates from scratch. Called after reconcile/restore.
    fn recompute_caches(&self) {
        let mut realized = 0.0;
        let mut unrealized = 0.0;
        let mut notional = 0.0;
        let mut active = 0u32;
        for entry in self.positions.iter() {
            realized += entry.realized_pnl;
            unrealized += entry.unrealized_pnl;
            notional += entry.size * entry.avg_entry_price;
            if entry.size > 0.0 {
                active += 1;
            }
        }
        *self.cached_realized_pnl.write().unwrap_or_else(|e| e.into_inner()) = realized;
        *self.cached_unrealized_pnl.write().unwrap_or_else(|e| e.into_inner()) = unrealized;
        *self.cached_total_notional.write().unwrap_or_else(|e| e.into_inner()) = notional;
        self.cached_active_count.store(active, Ordering::Release);
    }

    /// Process a fill and update the corresponding position.
    #[inline]
    pub fn apply_fill(&self, fill: &Fill) {
        // Guard against NaN/Inf corrupting position state
        if !fill.price.is_finite() || !fill.size.is_finite() || !fill.fee.is_finite() {
            tracing::warn!(
                fill_id = %fill.fill_id,
                price = fill.price,
                size = fill.size,
                fee = fill.fee,
                "rejecting fill with non-finite numeric field"
            );
            return;
        }
        let key = (fill.market_id.clone(), fill.side);

        match fill.order_side {
            OrderSide::Buy => self.apply_buy(fill, key),
            OrderSide::Sell => self.apply_sell(fill, key),
        }
    }

    fn apply_buy(&self, fill: &Fill, key: PositionKey) {
        let mut was_active = false;
        let mut old_notional = 0.0;
        let mut new_notional = 0.0;
        let mut is_new = false;

        self.positions
            .entry(key)
            .and_modify(|pos| {
                was_active = pos.size > 0.0;
                old_notional = pos.size * pos.avg_entry_price;
                let new_size = pos.size + fill.size;
                if new_size > 1e-10 {
                    pos.avg_entry_price =
                        (pos.avg_entry_price * pos.size + fill.price * fill.size) / new_size;
                    pos.size = new_size;
                } else {
                    pos.size = 0.0;
                    pos.avg_entry_price = 0.0;
                }
                new_notional = pos.size * pos.avg_entry_price;
            })
            .or_insert_with(|| {
                is_new = true;
                new_notional = fill.size * fill.price;
                Position {
                    market_id: fill.market_id.clone(),
                    side: fill.side,
                    size: fill.size,
                    avg_entry_price: fill.price,
                    realized_pnl: 0.0,
                    unrealized_pnl: 0.0,
                    token_id: fill.token_id.clone(),
                    exchange: fill.exchange.clone(),
                }
            });

        // Update cached notional
        if let Ok(mut n) = self.cached_total_notional.write() {
            *n += new_notional - old_notional;
        }
        // Update cached active count
        if is_new || (!was_active && new_notional > 0.0) {
            self.cached_active_count.fetch_add(1, Ordering::Release);
        }
    }

    fn apply_sell(&self, fill: &Fill, key: PositionKey) {
        let mut realized_delta = 0.0;
        let mut unrealized_delta = 0.0;
        let mut old_notional = 0.0;
        let mut new_notional = 0.0;
        let mut was_active = false;
        let mut now_active = false;
        let mut is_new = false;

        self.positions
            .entry(key)
            .and_modify(|pos| {
                was_active = pos.size > 0.0;
                old_notional = pos.size * pos.avg_entry_price;
                let effective_size = fill.size.min(pos.size);
                let realized = (fill.price - pos.avg_entry_price) * effective_size - fill.fee;
                realized_delta = realized;
                pos.realized_pnl += realized;
                pos.size = (pos.size - fill.size).max(0.0);
                // Scale unrealized PnL proportionally to size reduction
                let old_unrealized = pos.unrealized_pnl;
                if was_active && pos.size > 0.0 {
                    let old_total_size = pos.size + effective_size;
                    if old_total_size > 1e-10 {
                        pos.unrealized_pnl = old_unrealized * pos.size / old_total_size;
                    }
                } else {
                    pos.unrealized_pnl = 0.0;
                }
                unrealized_delta = pos.unrealized_pnl - old_unrealized;
                now_active = pos.size > 0.0;
                new_notional = pos.size * pos.avg_entry_price;
            })
            .or_insert_with(|| {
                is_new = true;
                realized_delta = -fill.fee;
                Position {
                    market_id: fill.market_id.clone(),
                    side: fill.side,
                    size: 0.0,
                    avg_entry_price: fill.price,
                    realized_pnl: -fill.fee,
                    unrealized_pnl: 0.0,
                    token_id: fill.token_id.clone(),
                    exchange: fill.exchange.clone(),
                }
            });

        // Update cached realized PnL
        if let Ok(mut r) = self.cached_realized_pnl.write() {
            *r += realized_delta;
        }
        // Update cached unrealized PnL
        if unrealized_delta.abs() > f64::EPSILON {
            if let Ok(mut u) = self.cached_unrealized_pnl.write() {
                *u += unrealized_delta;
            }
        }
        // Update cached notional
        if let Ok(mut n) = self.cached_total_notional.write() {
            *n += new_notional - old_notional;
        }
        // Update cached active count
        if was_active && !now_active && !is_new {
            self.cached_active_count.fetch_sub(1, Ordering::Release);
        }
    }

    /// Update unrealized P&L for a position given a current market price.
    #[inline]
    pub fn update_mark_price(&self, market_id: &str, side: Side, mark_price: f64) {
        if !mark_price.is_finite() {
            tracing::warn!(market_id = %market_id, mark_price = mark_price, "ignoring non-finite mark price");
            return;
        }
        let key = (market_id.to_string(), side);
        if let Some(mut pos) = self.positions.get_mut(&key) {
            let old_unrealized = pos.unrealized_pnl;
            let new_unrealized = (mark_price - pos.avg_entry_price) * pos.size;
            pos.unrealized_pnl = new_unrealized;
            // Update cache with delta
            if let Ok(mut u) = self.cached_unrealized_pnl.write() {
                *u += new_unrealized - old_unrealized;
            }
        }
    }

    /// Get average entry price for a market (weighted across YES/NO sides).
    pub fn avg_entry_price_for_market(&self, market_id: &str) -> f64 {
        let yes_key = (market_id.to_string(), Side::Yes);
        let no_key = (market_id.to_string(), Side::No);
        let mut total_notional = 0.0;
        let mut total_size = 0.0;
        if let Some(pos) = self.positions.get(&yes_key) {
            total_notional += pos.size * pos.avg_entry_price;
            total_size += pos.size;
        }
        if let Some(pos) = self.positions.get(&no_key) {
            total_notional += pos.size * pos.avg_entry_price;
            total_size += pos.size;
        }
        if total_size > 0.0 { total_notional / total_size } else { 0.0 }
    }

    /// Get total exposure for a market (sum of YES + NO positions).
    #[inline]
    pub fn market_exposure(&self, market_id: &str) -> f64 {
        let yes_key = (market_id.to_string(), Side::Yes);
        let no_key = (market_id.to_string(), Side::No);

        let yes = self
            .positions
            .get(&yes_key)
            .map(|p| p.size)
            .unwrap_or(0.0);
        let no = self
            .positions
            .get(&no_key)
            .map(|p| p.size)
            .unwrap_or(0.0);

        yes + no
    }

    /// Get total exposure across all outcome markets in an event.
    /// Sum of market_exposure() for each market_id in the slice.
    #[inline]
    pub fn event_exposure(&self, market_ids: &[String]) -> f64 {
        market_ids.iter().map(|mid| self.market_exposure(mid)).sum()
    }

    /// Get total portfolio notional (O(1) from cache).
    #[inline]
    pub fn total_notional(&self) -> f64 {
        *self.cached_total_notional.read().unwrap_or_else(|e| e.into_inner())
    }

    /// Snapshot market exposure and total notional.
    /// Both are O(1): market exposure via DashMap lookups, notional from cache.
    #[inline]
    pub fn snapshot_exposure(&self, market_id: &str) -> (f64, f64) {
        // O(1) lookups for target market exposure
        let mut exposure = 0.0;
        let yes_key = (market_id.to_string(), Side::Yes);
        let no_key = (market_id.to_string(), Side::No);
        if let Some(pos) = self.positions.get(&yes_key) {
            exposure += pos.size;
        }
        if let Some(pos) = self.positions.get(&no_key) {
            exposure += pos.size;
        }

        // O(1) from cache
        let notional = *self.cached_total_notional.read().unwrap_or_else(|e| e.into_inner());

        (exposure, notional)
    }

    /// Get total realized P&L (O(1) from cache).
    pub fn total_realized_pnl(&self) -> f64 {
        *self.cached_realized_pnl.read().unwrap_or_else(|e| e.into_inner())
    }

    /// Get total unrealized P&L (O(1) from cache).
    pub fn total_unrealized_pnl(&self) -> f64 {
        *self.cached_unrealized_pnl.read().unwrap_or_else(|e| e.into_inner())
    }

    /// Get positions for a specific market (O(1) — 2 DashMap lookups).
    pub fn positions_for_market(&self, market_id: &str) -> Vec<Position> {
        let mut result = Vec::with_capacity(2);
        let yes_key = (market_id.to_string(), Side::Yes);
        let no_key = (market_id.to_string(), Side::No);
        if let Some(pos) = self.positions.get(&yes_key) {
            if pos.size > 0.0 {
                result.push(pos.value().clone());
            }
        }
        if let Some(pos) = self.positions.get(&no_key) {
            if pos.size > 0.0 {
                result.push(pos.value().clone());
            }
        }
        result
    }

    /// Get unrealized PnL for a specific market (O(1) — 2 DashMap lookups).
    pub fn unrealized_pnl_for_market(&self, market_id: &str) -> f64 {
        let yes_key = (market_id.to_string(), Side::Yes);
        let no_key = (market_id.to_string(), Side::No);
        let yes_pnl = self.positions.get(&yes_key).map(|p| p.unrealized_pnl).unwrap_or(0.0);
        let no_pnl = self.positions.get(&no_key).map(|p| p.unrealized_pnl).unwrap_or(0.0);
        yes_pnl + no_pnl
    }

    /// Get all active positions (size > 0).
    pub fn all_positions(&self) -> Vec<Position> {
        self.positions
            .iter()
            .filter(|e| e.size > 0.0)
            .map(|e| e.value().clone())
            .collect()
    }

    /// Count active positions (O(1) from cache).
    pub fn active_count(&self) -> u32 {
        self.cached_active_count.load(Ordering::Acquire)
    }

    /// Get all positions including closed ones with realized PnL (for DB snapshots).
    pub fn all_positions_for_snapshot(&self) -> Vec<Position> {
        self.positions
            .iter()
            .filter(|e| e.size > 0.0 || e.realized_pnl.abs() > f64::EPSILON)
            .map(|e| e.value().clone())
            .collect()
    }

    /// Restore positions from a DB snapshot (crash recovery).
    /// Inserts positions as-is without overriding PnL values.
    pub fn restore_from_snapshot(&self, positions: Vec<Position>) {
        let count = positions.len();
        for pos in positions {
            let key = (pos.market_id.clone(), pos.side);
            self.positions.insert(key, pos);
        }
        self.recompute_caches();
        info!(count = count, "positions restored from snapshot");
    }

    /// Replace all positions (for reconciliation with exchange).
    /// Skips reconciliation if the input is empty to avoid wiping local state
    /// on API errors or timeouts.
    /// Builds the new map first, then swaps atomically to avoid race conditions
    /// where concurrent reads see an empty map between clear() and re-insert.
    pub fn reconcile(&self, positions: Vec<Position>) {
        if positions.is_empty() {
            warn!("reconcile called with empty positions, skipping");
            return;
        }
        // Preserve realized PnL from existing positions
        let existing_pnl: HashMap<PositionKey, f64> = self
            .positions
            .iter()
            .map(|e| (e.key().clone(), e.realized_pnl))
            .collect();

        // Build new position set, then swap atomically
        // First, insert/update all new positions
        let new_keys: Vec<PositionKey> = positions
            .iter()
            .map(|p| (p.market_id.clone(), p.side))
            .collect();

        for mut pos in positions {
            let key = (pos.market_id.clone(), pos.side);
            if let Some(&pnl) = existing_pnl.get(&key) {
                pos.realized_pnl = pnl;
            }
            self.positions.insert(key, pos);
        }

        // Remove keys that are no longer present (HashSet for O(1) lookups)
        let new_key_set: HashSet<&PositionKey> = new_keys.iter().collect();
        let stale_keys: Vec<PositionKey> = self
            .positions
            .iter()
            .map(|e| e.key().clone())
            .filter(|k| !new_key_set.contains(&k))
            .collect();
        for key in stale_keys {
            self.positions.remove(&key);
        }

        self.recompute_caches();
        info!(count = new_keys.len(), "positions reconciled");
    }
}

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

    fn buy_fill(market: &str, side: Side, size: f64, price: f64) -> Fill {
        Fill {
            fill_id: uuid::Uuid::new_v4().to_string(),
            order_id: "ord_1".into(),
            market_id: market.into(),
            side,
            order_side: OrderSide::Buy,
            price,
            size,
            fee: 0.01,
            timestamp: 0.0,
            token_id: None,
            exchange: String::new(),
            is_maker: false,
        }
    }

    fn sell_fill(market: &str, side: Side, size: f64, price: f64) -> Fill {
        Fill {
            fill_id: uuid::Uuid::new_v4().to_string(),
            order_id: "ord_1".into(),
            market_id: market.into(),
            side,
            order_side: OrderSide::Sell,
            price,
            size,
            fee: 0.01,
            timestamp: 0.0,
            token_id: None,
            exchange: String::new(),
            is_maker: false,
        }
    }

    #[test]
    fn new_position_from_fill() {
        let tracker = PositionTracker::new();
        tracker.apply_fill(&buy_fill("mkt_1", Side::Yes, 10.0, 0.55));

        assert_eq!(tracker.active_count(), 1);
        let positions = tracker.all_positions();
        assert!((positions[0].size - 10.0).abs() < 1e-10);
        assert!((positions[0].avg_entry_price - 0.55).abs() < 1e-10);
    }

    #[test]
    fn weighted_avg_price() {
        let tracker = PositionTracker::new();
        tracker.apply_fill(&buy_fill("mkt_1", Side::Yes, 10.0, 0.50));
        tracker.apply_fill(&buy_fill("mkt_1", Side::Yes, 10.0, 0.60));

        let positions = tracker.all_positions();
        assert!((positions[0].size - 20.0).abs() < 1e-10);
        assert!((positions[0].avg_entry_price - 0.55).abs() < 1e-10);
    }

    #[test]
    fn market_exposure() {
        let tracker = PositionTracker::new();
        tracker.apply_fill(&buy_fill("mkt_1", Side::Yes, 10.0, 0.50));
        tracker.apply_fill(&buy_fill("mkt_1", Side::No, 5.0, 0.45));

        assert!((tracker.market_exposure("mkt_1") - 15.0).abs() < 1e-10);
    }

    #[test]
    fn realized_pnl_on_sell() {
        let tracker = PositionTracker::new();
        tracker.apply_fill(&buy_fill("mkt_1", Side::Yes, 10.0, 0.50));
        tracker.apply_fill(&sell_fill("mkt_1", Side::Yes, 5.0, 0.60));

        let pnl = tracker.total_realized_pnl();
        // (0.60 - 0.50) * 5 - 0.01 fee = 0.49
        assert!((pnl - 0.49).abs() < 1e-10);
    }

    #[test]
    fn reconcile_empty_skips() {
        let tracker = PositionTracker::new();
        tracker.apply_fill(&buy_fill("mkt_1", Side::Yes, 10.0, 0.50));
        assert_eq!(tracker.active_count(), 1);

        // Empty reconcile should not wipe local state
        tracker.reconcile(Vec::new());
        assert_eq!(tracker.active_count(), 1);
    }

    #[test]
    fn reconcile_non_empty_replaces() {
        let tracker = PositionTracker::new();
        tracker.apply_fill(&buy_fill("mkt_1", Side::Yes, 10.0, 0.50));

        let new_pos = Position {
            market_id: "mkt_2".into(),
            side: Side::Yes,
            size: 5.0,
            avg_entry_price: 0.60,
            realized_pnl: 0.0,
            unrealized_pnl: 0.0,
            token_id: None,
            exchange: String::new(),
        };
        tracker.reconcile(vec![new_pos]);

        assert_eq!(tracker.active_count(), 1);
        let positions = tracker.all_positions();
        assert_eq!(positions[0].market_id, "mkt_2");
    }

    #[test]
    fn event_exposure_across_outcomes() {
        let tracker = PositionTracker::new();
        tracker.apply_fill(&buy_fill("outcome_a", Side::Yes, 10.0, 0.30));
        tracker.apply_fill(&buy_fill("outcome_b", Side::Yes, 5.0, 0.40));
        tracker.apply_fill(&buy_fill("outcome_c", Side::No, 3.0, 0.50));

        let market_ids = vec![
            "outcome_a".to_string(),
            "outcome_b".to_string(),
            "outcome_c".to_string(),
        ];
        let exposure = tracker.event_exposure(&market_ids);
        // 10 + 5 + 3 = 18
        assert!((exposure - 18.0).abs() < 1e-10);
    }

    #[test]
    fn event_exposure_empty() {
        let tracker = PositionTracker::new();
        let exposure = tracker.event_exposure(&[]);
        assert!((exposure).abs() < 1e-10);
    }

    #[test]
    fn sell_without_position_creates_entry() {
        let tracker = PositionTracker::new();
        // Sell fill for a market with no local position
        tracker.apply_fill(&sell_fill("mkt_1", Side::Yes, 5.0, 0.60));

        // Should create an entry with size 0 (no position to close)
        let key = ("mkt_1".to_string(), Side::Yes);
        assert!(tracker.positions.get(&key).is_some());
        let pos = tracker.positions.get(&key).unwrap();
        assert!((pos.size - 0.0).abs() < 1e-10);
        assert!((pos.realized_pnl - (-0.01)).abs() < 1e-10); // only fee
    }
}
