//! Asset Selection Module
//!
//! Implements asset selection logic to determine which instruments to trade from the universe.
//! Integrates ML predictions, technical indicators, and liquidity scoring to rank and select
//! the best trading opportunities.

use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use common::ml_strategy::{MLPrediction, SharedMLStrategy};
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use std::collections::HashMap;
use std::sync::Arc;
use tracing::{debug, info, warn};

/// Asset score with multiple dimensions
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AssetScore {
    /// Trading symbol
    pub symbol: String,
    /// ML prediction score (0.0-1.0)
    pub ml_score: f64,
    /// Technical momentum score (0.0-1.0)
    pub momentum_score: f64,
    /// Fundamental value score (0.0-1.0)
    pub value_score: f64,
    /// Trading liquidity score (0.0-1.0)
    pub liquidity_score: f64,
    /// Weighted composite score (0.0-1.0)
    pub composite_score: f64,
    /// Timestamp when score was calculated
    pub timestamp: DateTime<Utc>,
    /// Additional metadata
    pub metadata: HashMap<String, f64>,
}

/// Weighting configuration for composite scoring
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ScoringWeights {
    /// ML model weight (default: 0.4)
    pub ml_weight: f64,
    /// Momentum weight (default: 0.3)
    pub momentum_weight: f64,
    /// Value weight (default: 0.2)
    pub value_weight: f64,
    /// Liquidity weight (default: 0.1)
    pub liquidity_weight: f64,
}

impl Default for ScoringWeights {
    fn default() -> Self {
        Self {
            ml_weight: 0.4,
            momentum_weight: 0.3,
            value_weight: 0.2,
            liquidity_weight: 0.1,
        }
    }
}

impl ScoringWeights {
    /// Validate that weights sum to approximately 1.0
    pub fn validate(&self) -> Result<()> {
        let sum = self.ml_weight + self.momentum_weight + self.value_weight + self.liquidity_weight;
        if (sum - 1.0).abs() > 0.01 {
            anyhow::bail!("Scoring weights must sum to 1.0, got {}", sum);
        }
        Ok(())
    }

    /// Normalize weights to sum to 1.0
    pub fn normalize(&mut self) {
        let sum = self.ml_weight + self.momentum_weight + self.value_weight + self.liquidity_weight;
        if sum > 0.0 {
            self.ml_weight /= sum;
            self.momentum_weight /= sum;
            self.value_weight /= sum;
            self.liquidity_weight /= sum;
        }
    }
}

/// Market data for scoring calculations
#[derive(Debug, Clone)]
struct MarketData {
    symbol: String,
    current_price: f64,
    volume_24h: f64,
    prices_20d: Vec<f64>, // 20-day price history for momentum
}

/// Asset selector for choosing instruments from universe
pub struct AssetSelector {
    /// Database connection pool
    pool: PgPool,
    /// ML strategy for predictions
    ml_strategy: Arc<SharedMLStrategy>,
    /// Scoring weights configuration
    weights: ScoringWeights,
    /// ML prediction cache (symbol -> (timestamp, prediction))
    ml_cache: Arc<tokio::sync::RwLock<HashMap<String, (DateTime<Utc>, MLPrediction)>>>,
    /// Cache TTL in seconds
    cache_ttl_seconds: i64,
}

impl AssetSelector {
    /// Create new asset selector
    pub fn new(
        pool: PgPool,
        ml_strategy: Arc<SharedMLStrategy>,
        weights: Option<ScoringWeights>,
    ) -> Result<Self> {
        let mut weights = weights.unwrap_or_default();
        weights.normalize(); // Ensure weights sum to 1.0

        Ok(Self {
            pool,
            ml_strategy,
            weights,
            ml_cache: Arc::new(tokio::sync::RwLock::new(HashMap::new())),
            cache_ttl_seconds: 300, // 5 minutes default
        })
    }

    /// Select assets from universe based on composite scoring
    pub async fn select_assets(
        &self,
        universe_id: &str,
        max_assets: usize,
    ) -> Result<Vec<AssetScore>> {
        debug!(
            "Selecting up to {} assets from universe {}",
            max_assets, universe_id
        );

        // Get instruments from universe
        let symbols = self.get_universe_instruments(universe_id).await?;

        if symbols.is_empty() {
            info!("Universe {} has no instruments", universe_id);
            return Ok(Vec::new());
        }

        // Get market data for all symbols
        let market_data = self.fetch_market_data(&symbols).await?;

        // Query ML predictions (with caching)
        let ml_predictions = self.query_ml_predictions(&symbols).await?;

        // Calculate scores for each asset
        let mut asset_scores = Vec::new();
        for data in market_data {
            let ml_score = ml_predictions
                .get(&data.symbol)
                .map(|p| p.prediction_value)
                .unwrap_or(0.5); // Default to neutral if ML unavailable

            let momentum_score = self.calculate_momentum_score(&data)?;
            let liquidity_score = self.calculate_liquidity_score(&data)?;
            let value_score = self.calculate_value_score(&data)?;

            let composite_score = self.calculate_composite_score(
                ml_score,
                momentum_score,
                value_score,
                liquidity_score,
            );

            let mut metadata = HashMap::new();
            metadata.insert("current_price".to_string(), data.current_price);
            metadata.insert("volume_24h".to_string(), data.volume_24h);

            asset_scores.push(AssetScore {
                symbol: data.symbol.clone(),
                ml_score,
                momentum_score,
                value_score,
                liquidity_score,
                composite_score,
                timestamp: Utc::now(),
                metadata,
            });
        }

        // Sort by composite score (descending)
        asset_scores.sort_by(|a, b| {
            b.composite_score
                .partial_cmp(&a.composite_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });

        // Take top N assets
        let selected = asset_scores
            .into_iter()
            .take(max_assets)
            .collect::<Vec<_>>();

        // Store selection in database
        self.store_selection(universe_id, &selected).await?;

        Ok(selected)
    }

    /// Get selected assets by selection ID
    pub async fn get_selected_assets(&self, selection_id: &str) -> Result<Vec<AssetScore>> {
        let record = sqlx::query!(
            r#"
            SELECT asset_scores, selected_at
            FROM asset_selections
            WHERE id::text = $1
            "#,
            selection_id
        )
        .fetch_optional(&self.pool)
        .await
        .context("Failed to fetch selected assets")?;

        if let Some(record) = record {
            let assets: Vec<AssetScore> = serde_json::from_value(record.asset_scores)
                .context("Failed to deserialize asset scores")?;
            Ok(assets)
        } else {
            Ok(Vec::new())
        }
    }

    /// Query ML predictions for symbols (with caching)
    async fn query_ml_predictions(
        &self,
        symbols: &[String],
    ) -> Result<HashMap<String, MLPrediction>> {
        let mut predictions = HashMap::new();
        let now = Utc::now();

        // Check cache first
        let cache = self.ml_cache.read().await;
        let mut symbols_to_query = Vec::new();

        for symbol in symbols {
            if let Some((timestamp, prediction)) = cache.get(symbol) {
                if (now - *timestamp).num_seconds() < self.cache_ttl_seconds {
                    predictions.insert(symbol.clone(), prediction.clone());
                    continue;
                }
            }
            symbols_to_query.push(symbol.clone());
        }
        drop(cache);

        // Query ML for uncached symbols
        if !symbols_to_query.is_empty() {
            match self.query_ml_batch(&symbols_to_query).await {
                Ok(new_predictions) => {
                    let mut cache = self.ml_cache.write().await;
                    for (symbol, prediction) in new_predictions {
                        cache.insert(symbol.clone(), (now, prediction.clone()));
                        predictions.insert(symbol, prediction);
                    }
                },
                Err(e) => {
                    info!("ML service unavailable, using fallback scores: {}", e);
                    // Use technical scores only as fallback
                },
            }
        }

        Ok(predictions)
    }

    /// Query ML predictions in batch
    async fn query_ml_batch(&self, symbols: &[String]) -> Result<HashMap<String, MLPrediction>> {
        let mut predictions = HashMap::new();

        // For each symbol, get ensemble prediction
        for symbol in symbols {
            // Use default market data for ML query
            // In production, this would use real-time data
            let price = 100.0; // Placeholder
            let volume = 10000.0; // Placeholder

            match self
                .ml_strategy
                .get_ensemble_prediction(price, volume, Utc::now())
                .await
            {
                Ok(preds) if !preds.is_empty() => {
                    // Use the first prediction (or could use ensemble vote)
                    if let Some(first_pred) = preds.first() {
                        predictions.insert(symbol.clone(), first_pred.clone());
                    }
                },
                Ok(_) => {
                    debug!("No ML predictions for symbol {}", symbol);
                },
                Err(e) => {
                    warn!("Failed to get ML prediction for {}: {}", symbol, e);
                },
            }
        }

        Ok(predictions)
    }

    /// Calculate momentum score based on 20-day returns
    fn calculate_momentum_score(&self, data: &MarketData) -> Result<f64> {
        // Get oldest price or return neutral score
        let oldest_price = match data.prices_20d.first() {
            Some(price) => *price,
            None => return Ok(0.5), // Neutral score if no history
        };
        let current_price = data.current_price;

        if oldest_price == 0.0 {
            return Ok(0.5);
        }

        let return_20d = (current_price - oldest_price) / oldest_price;

        // Normalize to 0-1 range using sigmoid
        // Strong momentum: >10% return
        let normalized = 1.0 / (1.0 + (-return_20d * 10.0).exp());

        Ok(normalized)
    }

    /// Calculate liquidity score based on volume
    fn calculate_liquidity_score(&self, data: &MarketData) -> Result<f64> {
        // Normalize volume to 0-1 range
        // High liquidity: >$10M daily volume
        let volume_millions = data.volume_24h / 1_000_000.0;
        let score = (volume_millions / 10.0).min(1.0);

        Ok(score)
    }

    /// Calculate value score (placeholder for fundamental analysis)
    fn calculate_value_score(&self, _data: &MarketData) -> Result<f64> {
        // Placeholder: in production would use fundamental metrics
        // P/E ratio, book value, earnings, etc.
        Ok(0.5) // Neutral value score
    }

    /// Calculate composite score from component scores
    fn calculate_composite_score(
        &self,
        ml_score: f64,
        momentum_score: f64,
        value_score: f64,
        liquidity_score: f64,
    ) -> f64 {
        ml_score * self.weights.ml_weight
            + momentum_score * self.weights.momentum_weight
            + value_score * self.weights.value_weight
            + liquidity_score * self.weights.liquidity_weight
    }

    /// Get instruments from universe
    async fn get_universe_instruments(&self, universe_id: &str) -> Result<Vec<String>> {
        // Query database for universe instruments from JSONB column
        let record = sqlx::query!(
            r#"
            SELECT instruments
            FROM trading_universes
            WHERE universe_id = $1
            "#,
            universe_id
        )
        .fetch_optional(&self.pool)
        .await
        .context("Failed to fetch universe instruments")?;

        if let Some(record) = record {
            // Parse JSONB array of instruments
            let instruments: Vec<serde_json::Value> = serde_json::from_value(record.instruments)
                .context("Failed to deserialize instruments")?;

            // Extract symbols from instrument objects
            let symbols = instruments
                .into_iter()
                .filter_map(|inst| {
                    inst.as_object()
                        .and_then(|obj| obj.get("symbol"))
                        .and_then(|s| s.as_str())
                        .map(|s| s.to_string())
                })
                .collect();

            Ok(symbols)
        } else {
            Ok(Vec::new())
        }
    }

    /// Fetch market data for symbols
    async fn fetch_market_data(&self, symbols: &[String]) -> Result<Vec<MarketData>> {
        let mut market_data = Vec::new();

        for symbol in symbols {
            // Query latest market data for current price and volume
            let latest = sqlx::query!(
                r#"
                SELECT close_price, volume
                FROM market_data
                WHERE symbol = $1
                ORDER BY timestamp DESC
                LIMIT 1
                "#,
                symbol
            )
            .fetch_optional(&self.pool)
            .await
            .context("Failed to fetch latest market data")?;

            // Query 20-day price history for momentum calculation
            let history = sqlx::query!(
                r#"
                SELECT close_price
                FROM market_data
                WHERE symbol = $1
                ORDER BY timestamp DESC
                LIMIT 20
                "#,
                symbol
            )
            .fetch_all(&self.pool)
            .await
            .context("Failed to fetch price history")?;

            if let Some(latest_data) = latest {
                let current_price = {
                    use num_traits::ToPrimitive;
                    latest_data.close_price.to_f64().unwrap_or(0.0)
                };

                let volume_24h = {
                    use num_traits::ToPrimitive;
                    latest_data.volume.to_f64().unwrap_or(0.0)
                };

                let prices_20d = history
                    .iter()
                    .filter_map(|r| {
                        use num_traits::ToPrimitive;
                        r.close_price.to_f64()
                    })
                    .collect();

                market_data.push(MarketData {
                    symbol: symbol.clone(),
                    current_price,
                    volume_24h,
                    prices_20d,
                });
            }
        }

        Ok(market_data)
    }

    /// Store selection in database
    async fn store_selection(&self, universe_id: &str, assets: &[AssetScore]) -> Result<()> {
        // Serialize assets to JSONB
        let asset_scores_json =
            serde_json::to_value(assets).context("Failed to serialize asset scores")?;

        // Create criteria JSON (default for now)
        let criteria = serde_json::json!({
            "ml_weight": self.weights.ml_weight,
            "momentum_weight": self.weights.momentum_weight,
            "value_weight": self.weights.value_weight,
            "liquidity_weight": self.weights.liquidity_weight,
        });

        // Create metrics JSON
        let metrics = serde_json::json!({
            "total_assets": assets.len(),
            "avg_composite_score": if !assets.is_empty() {
                assets.iter().map(|a| a.composite_score).sum::<f64>() / assets.len() as f64
            } else {
                0.0
            },
            "avg_ml_score": if !assets.is_empty() {
                assets.iter().map(|a| a.ml_score).sum::<f64>() / assets.len() as f64
            } else {
                0.0
            },
        });

        sqlx::query!(
            r#"
            INSERT INTO asset_selections (universe_id, criteria, asset_scores, metrics)
            VALUES ($1, $2, $3, $4)
            "#,
            universe_id,
            criteria,
            asset_scores_json,
            metrics
        )
        .execute(&self.pool)
        .await
        .context("Failed to store asset selection")?;

        Ok(())
    }
}

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

    #[test]
    fn test_scoring_weights_default() {
        let weights = ScoringWeights::default();
        assert_eq!(weights.ml_weight, 0.4);
        assert_eq!(weights.momentum_weight, 0.3);
        assert_eq!(weights.value_weight, 0.2);
        assert_eq!(weights.liquidity_weight, 0.1);

        // Should sum to 1.0
        let sum = weights.ml_weight
            + weights.momentum_weight
            + weights.value_weight
            + weights.liquidity_weight;
        assert!((sum - 1.0).abs() < 0.001);
    }

    #[test]
    fn test_scoring_weights_normalize() {
        let mut weights = ScoringWeights {
            ml_weight: 2.0,
            momentum_weight: 1.0,
            value_weight: 1.0,
            liquidity_weight: 0.5,
        };

        weights.normalize();

        // Should sum to 1.0 after normalization
        let sum = weights.ml_weight
            + weights.momentum_weight
            + weights.value_weight
            + weights.liquidity_weight;
        assert!((sum - 1.0).abs() < 0.001);

        // Ratios should be preserved
        assert!((weights.ml_weight / weights.momentum_weight - 2.0).abs() < 0.001);
    }

    #[test]
    fn test_scoring_weights_validate() {
        let weights = ScoringWeights::default();
        assert!(weights.validate().is_ok());

        let invalid_weights = ScoringWeights {
            ml_weight: 0.5,
            momentum_weight: 0.3,
            value_weight: 0.2,
            liquidity_weight: 0.2, // Sum > 1.0
        };
        assert!(invalid_weights.validate().is_err());
    }

    #[test]
    fn test_asset_score_serialization() {
        let mut metadata = HashMap::new();
        metadata.insert("test_key".to_string(), 42.0);

        let score = AssetScore {
            symbol: "BTC".to_string(),
            ml_score: 0.75,
            momentum_score: 0.6,
            value_score: 0.5,
            liquidity_score: 0.9,
            composite_score: 0.7,
            timestamp: Utc::now(),
            metadata,
        };

        // Test serialization round-trip
        let json = serde_json::to_string(&score).expect("INVARIANT: Serialization should succeed for valid types");
        let deserialized: AssetScore = serde_json::from_str(&json).expect("INVARIANT: Deserialization should succeed for valid JSON");

        assert_eq!(deserialized.symbol, score.symbol);
        assert_eq!(deserialized.ml_score, score.ml_score);
        assert_eq!(deserialized.composite_score, score.composite_score);
    }
}
