//! Universe Selection Module
//!
//! Implements universe selection logic for determining which markets to trade.
//! Filters instruments based on liquidity, volatility, correlation, and other criteria.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::HashMap;
use uuid::Uuid;

use common::Symbol;

/// Error types for universe selection
#[derive(Debug, thiserror::Error)]
pub enum UniverseError {
    #[error("Database error: {0}")]
    Database(#[from] sqlx::Error),

    #[error("Invalid criteria: {0}")]
    InvalidCriteria(String),

    #[error("No instruments match criteria")]
    NoInstrumentsFound,

    #[error("Universe not found: {0}")]
    UniverseNotFound(String),

    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),
}

/// Asset class classification
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum AssetClass {
    Futures,
    Equities,
    Currencies,
    Commodities,
    Crypto,
}

/// Geographic region
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Region {
    NorthAmerica,
    Europe,
    Asia,
    Global,
}

/// Universe selection criteria
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UniverseCriteria {
    /// Minimum liquidity score (0.0-1.0)
    pub min_liquidity: f64,

    /// Maximum volatility (0.0-1.0)
    pub max_volatility: f64,

    /// Allowed asset classes
    pub asset_classes: Vec<AssetClass>,

    /// Allowed regions
    pub regions: Vec<Region>,

    /// Minimum market cap (optional)
    pub min_market_cap: Option<f64>,

    /// Maximum correlation threshold (optional)
    pub max_correlation: Option<f64>,
}

impl Default for UniverseCriteria {
    fn default() -> Self {
        Self {
            min_liquidity: 0.5,
            max_volatility: 0.8,
            asset_classes: vec![AssetClass::Futures],
            regions: vec![Region::NorthAmerica],
            min_market_cap: Some(1_000_000_000.0), // $1B
            max_correlation: Some(0.85),
        }
    }
}

/// Instrument metadata for universe selection
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Instrument {
    pub symbol: Symbol,
    pub exchange: String,
    pub asset_class: AssetClass,
    pub region: Region,
    pub liquidity_score: f64,
    pub volatility: f64,
    pub market_cap: Option<f64>,
    pub avg_daily_volume: f64,
    pub spread_bps: f64, // Bid-ask spread in basis points
}

/// Universe metrics for quality assessment
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UniverseMetrics {
    pub total_instruments: usize,
    pub avg_liquidity_score: f64,
    pub avg_volatility: f64,
    pub avg_spread_bps: f64,
    pub asset_class_distribution: HashMap<String, usize>,
    pub region_distribution: HashMap<String, usize>,
}

/// Selected universe with instruments and metrics
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Universe {
    pub universe_id: String,
    pub criteria: UniverseCriteria,
    pub instruments: Vec<Instrument>,
    pub metrics: UniverseMetrics,
    pub created_at: DateTime<Utc>,
    pub updated_at: DateTime<Utc>,
}

/// Universe selector implementation
pub struct UniverseSelector {
    pool: PgPool,
}

impl UniverseSelector {
    /// Create a new universe selector
    pub fn new(pool: PgPool) -> Self {
        Self { pool }
    }

    /// Select universe based on criteria
    ///
    /// # Arguments
    /// * `criteria` - Universe selection criteria
    ///
    /// # Returns
    /// Selected universe with instruments and metrics
    ///
    /// # Errors
    /// Returns error if database query fails or no instruments match
    pub async fn select_universe(
        &self,
        criteria: UniverseCriteria,
    ) -> Result<Universe, UniverseError> {
        // Validate criteria
        self.validate_criteria(&criteria)?;

        // For MVP, we'll use a hardcoded set of instruments
        // In production, this would query market data APIs
        let candidate_instruments = self.get_candidate_instruments().await?;

        // Apply filters
        let filtered_instruments = self.apply_filters(&candidate_instruments, &criteria);

        // Check if we have any instruments
        if filtered_instruments.is_empty() {
            return Err(UniverseError::NoInstrumentsFound);
        }

        // Calculate metrics
        let metrics = self.calculate_metrics(&filtered_instruments);

        // Create universe
        let universe_id = format!("universe_{}", Uuid::new_v4());
        let now = Utc::now();

        let universe = Universe {
            universe_id: universe_id.clone(),
            criteria: criteria.clone(),
            instruments: filtered_instruments,
            metrics,
            created_at: now,
            updated_at: now,
        };

        // Store in database
        self.store_universe(&universe).await?;

        Ok(universe)
    }

    /// Get universe by ID
    pub async fn get_universe(&self, universe_id: &str) -> Result<Universe, UniverseError> {
        let row = sqlx::query!(
            r#"
            SELECT universe_id, criteria, instruments, metrics, created_at, updated_at
            FROM trading_universes
            WHERE universe_id = $1
            "#,
            universe_id
        )
        .fetch_optional(&self.pool)
        .await?;

        match row {
            Some(row) => {
                let criteria: UniverseCriteria = serde_json::from_value(row.criteria)?;
                let instruments: Vec<Instrument> = serde_json::from_value(row.instruments)?;
                let metrics: UniverseMetrics = serde_json::from_value(row.metrics)?;

                Ok(Universe {
                    universe_id: row.universe_id,
                    criteria,
                    instruments,
                    metrics,
                    created_at: row.created_at,
                    updated_at: row.updated_at,
                })
            },
            None => Err(UniverseError::UniverseNotFound(universe_id.to_string())),
        }
    }

    /// Update universe criteria and reselect
    pub async fn update_criteria(
        &self,
        universe_id: &str,
        new_criteria: UniverseCriteria,
    ) -> Result<Universe, UniverseError> {
        // Verify universe exists
        let _ = self.get_universe(universe_id).await?;

        // Create new universe with updated criteria
        let new_universe = self.select_universe(new_criteria).await?;

        Ok(new_universe)
    }

    /// Validate criteria
    fn validate_criteria(&self, criteria: &UniverseCriteria) -> Result<(), UniverseError> {
        if !(0.0..=1.0).contains(&criteria.min_liquidity) {
            return Err(UniverseError::InvalidCriteria(
                "min_liquidity must be between 0.0 and 1.0".to_string(),
            ));
        }

        if !(0.0..=1.0).contains(&criteria.max_volatility) {
            return Err(UniverseError::InvalidCriteria(
                "max_volatility must be between 0.0 and 1.0".to_string(),
            ));
        }

        if criteria.asset_classes.is_empty() {
            return Err(UniverseError::InvalidCriteria(
                "At least one asset class must be specified".to_string(),
            ));
        }

        if criteria.regions.is_empty() {
            return Err(UniverseError::InvalidCriteria(
                "At least one region must be specified".to_string(),
            ));
        }

        if let Some(max_corr) = criteria.max_correlation {
            if !(0.0..=1.0).contains(&max_corr) {
                return Err(UniverseError::InvalidCriteria(
                    "max_correlation must be between 0.0 and 1.0".to_string(),
                ));
            }
        }

        Ok(())
    }

    /// Get candidate instruments (MVP: hardcoded, production: query market data)
    async fn get_candidate_instruments(&self) -> Result<Vec<Instrument>, UniverseError> {
        // MVP: Return hardcoded instruments based on available data
        Ok(vec![
            Instrument {
                symbol: "ES.FUT".into(),
                exchange: "CME".to_string(),
                asset_class: AssetClass::Futures,
                region: Region::NorthAmerica,
                liquidity_score: 0.95,
                volatility: 0.20,
                market_cap: Some(10_000_000_000.0),
                avg_daily_volume: 2_000_000.0,
                spread_bps: 0.5,
            },
            Instrument {
                symbol: "NQ.FUT".into(),
                exchange: "CME".to_string(),
                asset_class: AssetClass::Futures,
                region: Region::NorthAmerica,
                liquidity_score: 0.92,
                volatility: 0.25,
                market_cap: Some(8_000_000_000.0),
                avg_daily_volume: 1_500_000.0,
                spread_bps: 0.8,
            },
            Instrument {
                symbol: "ZN.FUT".into(),
                exchange: "CME".to_string(),
                asset_class: AssetClass::Futures,
                region: Region::NorthAmerica,
                liquidity_score: 0.88,
                volatility: 0.15,
                market_cap: Some(5_000_000_000.0),
                avg_daily_volume: 800_000.0,
                spread_bps: 1.0,
            },
            Instrument {
                symbol: "6E.FUT".into(),
                exchange: "CME".to_string(),
                asset_class: AssetClass::Currencies,
                region: Region::Global,
                liquidity_score: 0.85,
                volatility: 0.18,
                market_cap: Some(4_000_000_000.0),
                avg_daily_volume: 600_000.0,
                spread_bps: 1.2,
            },
            Instrument {
                symbol: "CL.FUT".into(),
                exchange: "CME".to_string(),
                asset_class: AssetClass::Commodities,
                region: Region::Global,
                liquidity_score: 0.90,
                volatility: 0.35,
                market_cap: Some(6_000_000_000.0),
                avg_daily_volume: 1_200_000.0,
                spread_bps: 0.6,
            },
        ])
    }

    /// Apply filters to candidate instruments
    fn apply_filters(
        &self,
        instruments: &[Instrument],
        criteria: &UniverseCriteria,
    ) -> Vec<Instrument> {
        instruments
            .iter()
            .filter(|inst| {
                // Filter by liquidity
                if inst.liquidity_score < criteria.min_liquidity {
                    return false;
                }

                // Filter by volatility
                if inst.volatility > criteria.max_volatility {
                    return false;
                }

                // Filter by asset class
                if !criteria.asset_classes.contains(&inst.asset_class) {
                    return false;
                }

                // Filter by region
                if !criteria.regions.contains(&inst.region) {
                    return false;
                }

                // Filter by market cap
                if let Some(min_cap) = criteria.min_market_cap {
                    if let Some(cap) = inst.market_cap {
                        if cap < min_cap {
                            return false;
                        }
                    } else {
                        return false; // No market cap data
                    }
                }

                true
            })
            .cloned()
            .collect()
    }

    /// Calculate universe metrics
    fn calculate_metrics(&self, instruments: &[Instrument]) -> UniverseMetrics {
        let total_instruments = instruments.len();

        if total_instruments == 0 {
            return UniverseMetrics {
                total_instruments: 0,
                avg_liquidity_score: 0.0,
                avg_volatility: 0.0,
                avg_spread_bps: 0.0,
                asset_class_distribution: HashMap::new(),
                region_distribution: HashMap::new(),
            };
        }

        let avg_liquidity_score =
            instruments.iter().map(|i| i.liquidity_score).sum::<f64>() / total_instruments as f64;

        let avg_volatility =
            instruments.iter().map(|i| i.volatility).sum::<f64>() / total_instruments as f64;

        let avg_spread_bps =
            instruments.iter().map(|i| i.spread_bps).sum::<f64>() / total_instruments as f64;

        let mut asset_class_distribution = HashMap::new();
        for inst in instruments {
            let key = format!("{:?}", inst.asset_class);
            *asset_class_distribution.entry(key).or_insert(0) += 1;
        }

        let mut region_distribution = HashMap::new();
        for inst in instruments {
            let key = format!("{:?}", inst.region);
            *region_distribution.entry(key).or_insert(0) += 1;
        }

        UniverseMetrics {
            total_instruments,
            avg_liquidity_score,
            avg_volatility,
            avg_spread_bps,
            asset_class_distribution,
            region_distribution,
        }
    }

    /// Store universe in database
    async fn store_universe(&self, universe: &Universe) -> Result<(), UniverseError> {
        let criteria_json = serde_json::to_value(&universe.criteria)?;
        let instruments_json = serde_json::to_value(&universe.instruments)?;
        let metrics_json = serde_json::to_value(&universe.metrics)?;

        sqlx::query!(
            r#"
            INSERT INTO trading_universes (
                universe_id, criteria, instruments, metrics, created_at, updated_at
            )
            VALUES ($1, $2, $3, $4, $5, $6)
            ON CONFLICT (universe_id) DO UPDATE
            SET criteria = EXCLUDED.criteria,
                instruments = EXCLUDED.instruments,
                metrics = EXCLUDED.metrics,
                updated_at = EXCLUDED.updated_at
            "#,
            universe.universe_id,
            criteria_json,
            instruments_json,
            metrics_json,
            universe.created_at,
            universe.updated_at,
        )
        .execute(&self.pool)
        .await?;

        Ok(())
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;

    #[test]
    fn test_default_criteria() {
        let criteria = UniverseCriteria::default();
        assert_eq!(criteria.min_liquidity, 0.5);
        assert_eq!(criteria.max_volatility, 0.8);
        assert_eq!(criteria.asset_classes.len(), 1);
        assert_eq!(criteria.regions.len(), 1);
    }

    #[test]
    fn test_validate_criteria_valid() {
        // Wrap in tokio runtime to avoid "requires a Tokio context" error from PgPool::connect_lazy
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let pool = PgPool::connect_lazy("postgresql://localhost/test")
                .expect("Failed to create lazy pool for test - database URL invalid");
            let selector = UniverseSelector { pool };

            let criteria = UniverseCriteria::default();
            assert!(selector.validate_criteria(&criteria).is_ok());
        });
    }

    #[test]
    fn test_validate_criteria_invalid_liquidity() {
        // Wrap in tokio runtime to avoid "requires a Tokio context" error from PgPool::connect_lazy
        let rt = tokio::runtime::Runtime::new().unwrap();
        rt.block_on(async {
            let pool = PgPool::connect_lazy("postgresql://localhost/test")
                .expect("Failed to create lazy pool for test - database URL invalid");
            let selector = UniverseSelector { pool };

            let criteria = UniverseCriteria {
                min_liquidity: 1.5, // Invalid
                ..UniverseCriteria::default()
            };

            assert!(selector.validate_criteria(&criteria).is_err());
        });
    }

    #[tokio::test]
    async fn test_apply_filters_liquidity() {
        let selector = UniverseSelector {
            pool: PgPool::connect_lazy("postgresql://localhost/test")
                .expect("Failed to create lazy pool for test - database URL invalid"),
        };

        let instruments = selector
            .get_candidate_instruments()
            .await
            .expect("Failed to get candidates");

        let criteria = UniverseCriteria {
            min_liquidity: 0.9, // High threshold
            ..UniverseCriteria::default()
        };

        let filtered = selector.apply_filters(&instruments, &criteria);

        // All instruments should have liquidity >= 0.9
        for inst in &filtered {
            assert!(inst.liquidity_score >= 0.9);
        }
    }

    #[tokio::test]
    async fn test_calculate_metrics() {
        let selector = UniverseSelector {
            pool: PgPool::connect_lazy("postgresql://localhost/test")
                .expect("Failed to create lazy pool for test - database URL invalid"),
        };

        let instruments = selector
            .get_candidate_instruments()
            .await
            .expect("Failed to get candidates");
        let metrics = selector.calculate_metrics(&instruments);

        assert_eq!(metrics.total_instruments, instruments.len());
        assert!(metrics.avg_liquidity_score > 0.0);
        assert!(metrics.avg_volatility > 0.0);
    }
}
