use std::collections::HashMap;
use std::sync::Arc;

use anyhow::Result;
use serde::Serialize;

use crate::db::SqliteDb;
use crate::types::{QueryType, SearchResponse, SearchStrategy};

/// Tracks search query outcomes and learns optimal strategies per query type.
///
/// After accumulating enough samples (`min_samples_for_learning`), the metrics
/// system can recommend the best-performing strategy for each query type based
/// on historical success rate, latency, and cost data.
pub struct SearchMetrics {
    db: Arc<SqliteDb>,
    min_samples_for_learning: usize,
}

/// Aggregated performance statistics for a particular strategy on a query type.
#[derive(Debug, Clone, Serialize)]
pub struct StrategyStats {
    pub strategy: String,
    pub sample_count: usize,
    pub success_rate: f64,
    pub avg_latency_ms: f64,
    pub avg_cost_cents: f64,
    pub avg_result_count: f64,
}

/// A snapshot of metrics for a single recorded search.
#[derive(Debug, Clone, Serialize)]
pub struct SearchRecord {
    pub query_type: String,
    pub strategy: String,
    pub tier1_tried: bool,
    pub tier1_success: bool,
    pub tier2_tried: bool,
    pub tier2_success: bool,
    pub tier3_tried: bool,
    pub tier3_success: bool,
    pub latency_ms: i64,
    pub cost_cents: i64,
    pub result_count: i64,
    pub created_at: i64,
    pub user_satisfied: Option<bool>,
}

impl SearchMetrics {
    /// Create a new `SearchMetrics` backed by the given SQLite database.
    pub fn new(db: Arc<SqliteDb>) -> Self {
        Self {
            db,
            min_samples_for_learning: 100,
        }
    }

    /// Create a new `SearchMetrics` with a custom minimum-sample threshold for
    /// strategy learning. Useful in tests or for small corpora.
    pub fn with_min_samples(db: Arc<SqliteDb>, min_samples: usize) -> Self {
        Self {
            db,
            min_samples_for_learning: min_samples,
        }
    }

    /// Record a search query and its outcome for adaptive learning.
    ///
    /// Each call writes a row into `search_queries` with full tier-breakdown
    /// data so that future strategy recommendations can be computed.
    /// Returns the generated query ID so callers can include it in responses.
    pub async fn record_search(
        &self,
        query: &str,
        query_type: &QueryType,
        strategy: &SearchStrategy,
        response: &SearchResponse,
    ) -> Result<String> {
        let id = ulid::Ulid::new().to_string();
        let query_type_str = format!("{:?}", query_type);
        let strategy_str = format!("{:?}", strategy);

        self.db
            .record_search_query(
                &id,
                query,
                &query_type_str,
                &strategy_str,
                response.tier_breakdown.tier1_tried,
                response.tier_breakdown.tier1_success,
                response.tier_breakdown.tier2_tried,
                response.tier_breakdown.tier2_success,
                response.tier_breakdown.tier3_tried,
                response.tier_breakdown.tier3_success,
                response.latency.as_millis() as i64,
                (response.cost * 100.0) as i64,
                response.results.len() as i64,
            )
            .await?;

        tracing::info!(
            query = %query,
            query_type = %query_type_str,
            strategy = %strategy_str,
            latency_ms = response.latency.as_millis() as i64,
            result_count = response.results.len(),
            "Search event recorded"
        );

        // After recording, attempt to update the learned strategy for this
        // query type if we have enough data. Fire-and-forget; a failure here
        // should not break the caller.
        if let Err(e) = self.maybe_update_learned_strategy(&query_type_str).await {
            tracing::warn!(
                "Failed to update learned strategy for {}: {}",
                query_type_str,
                e
            );
        }

        Ok(id)
    }

    /// Get the learned best strategy for a query type, if one has been recorded.
    pub async fn get_learned_strategy(
        &self,
        query_type: &QueryType,
    ) -> Result<Option<SearchStrategy>> {
        let query_type_str = format!("{:?}", query_type);
        match self.db.get_learned_strategy(&query_type_str).await? {
            Some(s) => Ok(Self::parse_strategy(&s)),
            None => Ok(None),
        }
    }

    /// Update the `learned_strategies` table by computing aggregate metrics
    /// for the given query type, then upserting the winning strategy.
    ///
    /// This is a convenience method for callers that already know the outcome
    /// and want to directly update strategy stats without going through the
    /// full `record_search` -> `maybe_update_learned_strategy` pipeline.
    pub async fn update_learned_strategy(
        &self,
        query_type: &QueryType,
        strategy: &SearchStrategy,
        success: bool,
        latency: std::time::Duration,
        cost: f64,
    ) -> Result<()> {
        let pattern = format!("{:?}", query_type);
        let strategy_str = format!("{:?}", strategy);
        let latency_ms = latency.as_millis() as i64;
        let cost_cents = (cost * 100.0) as i64;

        // Simple incremental update: fetch existing, blend, and upsert.
        let (prev_rate, prev_latency, prev_cost, prev_count) =
            match self.db.get_learned_strategy(&pattern).await? {
                Some(_existing) => {
                    // We don't store the full stats in the lookup, so start
                    // with a neutral baseline.  In a production system we
                    // would SELECT the full row from learned_strategies.
                    (0.5_f64, latency_ms, cost_cents, 1_i64)
                }
                None => (0.0, 0, 0, 0),
            };

        let new_count = prev_count + 1;
        let success_val = if success { 1.0 } else { 0.0 };
        let new_rate = if new_count == 1 {
            success_val
        } else {
            (prev_rate * prev_count as f64 + success_val) / new_count as f64
        };
        let new_latency = if new_count == 1 {
            latency_ms
        } else {
            (prev_latency * prev_count + latency_ms) / new_count
        };
        let new_cost = if new_count == 1 {
            cost_cents
        } else {
            (prev_cost * prev_count + cost_cents) / new_count
        };

        self.db
            .upsert_learned_strategy(
                &pattern,
                &strategy_str,
                new_rate,
                new_latency,
                new_cost,
                new_count,
            )
            .await?;

        tracing::debug!(
            pattern = %pattern,
            strategy = %strategy_str,
            success_rate = new_rate,
            sample_count = new_count,
            "Learned strategy updated"
        );

        Ok(())
    }

    /// Attempt to recalculate and persist the best strategy for a query type.
    ///
    /// Only updates when we have at least `min_samples_for_learning` records.
    /// Public so that the feedback handler can trigger re-learning after
    /// recording user satisfaction.
    pub async fn maybe_update_learned_strategy(&self, query_type: &str) -> Result<()> {
        let records = self.collect_records_for_type(query_type).await?;

        if records.len() < self.min_samples_for_learning {
            return Ok(());
        }

        // Group records by strategy and compute aggregated stats.
        let stats = Self::aggregate_by_strategy(&records);

        // Pick the strategy with the best composite score.
        if let Some(best) = Self::pick_best_strategy(&stats) {
            self.db
                .upsert_learned_strategy(
                    query_type,
                    &best.strategy,
                    best.success_rate,
                    best.avg_latency_ms as i64,
                    best.avg_cost_cents as i64,
                    best.sample_count as i64,
                )
                .await?;

            tracing::info!(
                "Updated learned strategy for {}: {} (success_rate={:.2}, samples={})",
                query_type,
                best.strategy,
                best.success_rate,
                best.sample_count,
            );
        }

        Ok(())
    }

    /// Collect raw search records for a given query type from the database.
    async fn collect_records_for_type(&self, query_type: &str) -> Result<Vec<SearchRecord>> {
        self.db
            .get_search_records_by_type(query_type, 1000)
            .await
    }

    /// Compute a composite success score for a single search record.
    /// Combines result presence (40%), user satisfaction (40%), and latency bonus (20%).
    fn compute_success(record: &SearchRecord) -> f64 {
        let has_results = if record.result_count > 0 { 1.0 } else { 0.0 };
        let satisfaction = match record.user_satisfied {
            Some(true) => 1.0,
            Some(false) => 0.0,
            None => 0.5, // neutral when no feedback
        };
        let latency_bonus = ((2000.0 - record.latency_ms as f64) / 1800.0).clamp(0.0, 1.0);
        has_results * 0.4 + satisfaction * 0.4 + latency_bonus * 0.2
    }

    /// Aggregate records using exponentially-weighted temporal decay.
    /// Half-life: 14 days — recent searches dominate, old data fades naturally.
    fn aggregate_by_strategy(records: &[SearchRecord]) -> Vec<StrategyStats> {
        let mut groups: HashMap<String, Vec<&SearchRecord>> = HashMap::new();
        for r in records {
            groups.entry(r.strategy.clone()).or_default().push(r);
        }

        let now = chrono::Utc::now().timestamp();
        // lambda = ln(2) / (14 days in seconds)
        let lambda = std::f64::consts::LN_2 / (14.0 * 86400.0);

        groups
            .into_iter()
            .map(|(strategy, recs)| {
                let mut total_weight = 0.0_f64;
                let mut weighted_success = 0.0_f64;
                let mut weighted_latency = 0.0_f64;
                let mut weighted_cost = 0.0_f64;
                let mut weighted_results = 0.0_f64;

                for r in &recs {
                    let age_secs = (now - r.created_at).max(0) as f64;
                    let weight = (-lambda * age_secs).exp();
                    total_weight += weight;

                    weighted_success += weight * Self::compute_success(r);
                    weighted_latency += weight * r.latency_ms as f64;
                    weighted_cost += weight * r.cost_cents as f64;
                    weighted_results += weight * r.result_count as f64;
                }

                let tw = total_weight.max(f64::EPSILON);

                StrategyStats {
                    strategy,
                    sample_count: recs.len(),
                    success_rate: weighted_success / tw,
                    avg_latency_ms: weighted_latency / tw,
                    avg_cost_cents: weighted_cost / tw,
                    avg_result_count: weighted_results / tw,
                }
            })
            .collect()
    }

    /// Pick the best strategy using a composite score.
    ///
    /// Score = success_rate * 0.6  -  normalized_latency * 0.25  -  normalized_cost * 0.15
    ///
    /// Higher is better.
    fn pick_best_strategy(stats: &[StrategyStats]) -> Option<&StrategyStats> {
        if stats.is_empty() {
            return None;
        }

        let max_latency = stats
            .iter()
            .map(|s| s.avg_latency_ms)
            .fold(f64::MIN, f64::max)
            .max(1.0);

        let max_cost = stats
            .iter()
            .map(|s| s.avg_cost_cents)
            .fold(f64::MIN, f64::max)
            .max(1.0);

        stats
            .iter()
            .max_by(|a, b| {
                let score_a = Self::composite_score(a, max_latency, max_cost);
                let score_b = Self::composite_score(b, max_latency, max_cost);
                score_a
                    .partial_cmp(&score_b)
                    .unwrap_or(std::cmp::Ordering::Equal)
            })
    }

    fn composite_score(stats: &StrategyStats, max_latency: f64, max_cost: f64) -> f64 {
        let norm_latency = stats.avg_latency_ms / max_latency;
        let norm_cost = stats.avg_cost_cents / max_cost;
        stats.success_rate * 0.6 - norm_latency * 0.25 - norm_cost * 0.15
    }

    /// Parse a strategy string back into a `SearchStrategy` enum variant.
    fn parse_strategy(s: &str) -> Option<SearchStrategy> {
        match s {
            "Tier1Only" => Some(SearchStrategy::Tier1Only),
            "Tier1ThenTier2" => Some(SearchStrategy::Tier1ThenTier2),
            "Tier1ThenTier2ThenTier3" => Some(SearchStrategy::Tier1ThenTier2ThenTier3),
            "Tier2Only" => Some(SearchStrategy::Tier2Only),
            "Tier3Only" => Some(SearchStrategy::Tier3Only),
            "GraphQuery" => Some(SearchStrategy::GraphQuery),
            _ => None,
        }
    }
}

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

    #[test]
    fn test_parse_strategy_round_trips() {
        let variants = [
            ("Tier1Only", SearchStrategy::Tier1Only),
            ("Tier1ThenTier2", SearchStrategy::Tier1ThenTier2),
            (
                "Tier1ThenTier2ThenTier3",
                SearchStrategy::Tier1ThenTier2ThenTier3,
            ),
            ("Tier2Only", SearchStrategy::Tier2Only),
            ("Tier3Only", SearchStrategy::Tier3Only),
            ("GraphQuery", SearchStrategy::GraphQuery),
        ];

        for (s, expected) in &variants {
            let parsed = SearchMetrics::parse_strategy(s);
            assert_eq!(parsed.as_ref(), Some(expected));
        }

        assert_eq!(SearchMetrics::parse_strategy("Unknown"), None);
    }

    #[test]
    fn test_aggregate_by_strategy() {
        let records = vec![
            SearchRecord {
                query_type: "Semantic".into(),
                strategy: "Tier1Only".into(),
                tier1_tried: true,
                tier1_success: true,
                tier2_tried: false,
                tier2_success: false,
                tier3_tried: false,
                tier3_success: false,
                latency_ms: 50,
                cost_cents: 1,
                result_count: 5,
                created_at: Utc::now().timestamp(),
                user_satisfied: None,
            },
            SearchRecord {
                query_type: "Semantic".into(),
                strategy: "Tier1Only".into(),
                tier1_tried: true,
                tier1_success: false,
                tier2_tried: false,
                tier2_success: false,
                tier3_tried: false,
                tier3_success: false,
                latency_ms: 80,
                cost_cents: 1,
                result_count: 0,
                created_at: Utc::now().timestamp(),
                user_satisfied: None,
            },
        ];

        let stats = SearchMetrics::aggregate_by_strategy(&records);
        assert_eq!(stats.len(), 1);
        let s = &stats[0];
        assert_eq!(s.sample_count, 2);
        // With composite success: record1=0.8, record2=0.4 → weighted avg ≈ 0.6
        assert!((s.success_rate - 0.6).abs() < 0.01, "got {}", s.success_rate);
        // Latency weighted avg ≈ 65 (both records created "now" so weights ≈ equal)
        assert!((s.avg_latency_ms - 65.0).abs() < 1.0, "got {}", s.avg_latency_ms);
    }

    #[test]
    fn test_success_metric_combines_signals() {
        // All positive: results + satisfied + fast
        let record = SearchRecord {
            query_type: "Semantic".into(),
            strategy: "Tier1Only".into(),
            tier1_tried: true,
            tier1_success: true,
            tier2_tried: false,
            tier2_success: false,
            tier3_tried: false,
            tier3_success: false,
            latency_ms: 200,
            cost_cents: 1,
            result_count: 10,
            created_at: Utc::now().timestamp(),
            user_satisfied: Some(true),
        };
        let score = SearchMetrics::compute_success(&record);
        // has_results(0.4) + satisfied(0.4) + latency_bonus(~0.2) = ~1.0
        assert!(score > 0.9, "got {}", score);

        // No results, unsatisfied
        let bad = SearchRecord {
            result_count: 0,
            user_satisfied: Some(false),
            latency_ms: 5000,
            ..record.clone()
        };
        let bad_score = SearchMetrics::compute_success(&bad);
        assert!(bad_score < 0.1, "got {}", bad_score);
    }

    #[test]
    fn test_user_satisfied_none_is_neutral() {
        let record = SearchRecord {
            query_type: "Semantic".into(),
            strategy: "Tier1Only".into(),
            tier1_tried: true,
            tier1_success: true,
            tier2_tried: false,
            tier2_success: false,
            tier3_tried: false,
            tier3_success: false,
            latency_ms: 100,
            cost_cents: 0,
            result_count: 5,
            created_at: Utc::now().timestamp(),
            user_satisfied: None,
        };
        let score = SearchMetrics::compute_success(&record);
        // None → 0.5 (neutral), so score should be moderate
        assert!(score > 0.5 && score < 1.0, "got {}", score);
    }

    #[test]
    fn test_temporal_decay_recent_weighted_more() {
        let now = Utc::now().timestamp();

        // Record from now: high success
        let recent = SearchRecord {
            query_type: "Semantic".into(),
            strategy: "Tier1Only".into(),
            tier1_tried: true,
            tier1_success: true,
            tier2_tried: false,
            tier2_success: false,
            tier3_tried: false,
            tier3_success: false,
            latency_ms: 50,
            cost_cents: 1,
            result_count: 10,
            created_at: now,
            user_satisfied: Some(true),
        };

        // Record from 60 days ago: no results
        let old = SearchRecord {
            result_count: 0,
            user_satisfied: Some(false),
            latency_ms: 3000,
            created_at: now - (60 * 86400),
            ..recent.clone()
        };

        let stats = SearchMetrics::aggregate_by_strategy(&[recent, old]);
        assert_eq!(stats.len(), 1);
        let s = &stats[0];
        // Recent record should dominate due to decay (60 days ≈ 4 half-lives → old weight ≈ 0.06)
        assert!(s.success_rate > 0.7, "Recent should dominate: got {}", s.success_rate);
    }

    #[test]
    fn test_pick_best_strategy_prefers_high_success() {
        let stats = vec![
            StrategyStats {
                strategy: "Slow".into(),
                sample_count: 100,
                success_rate: 0.95,
                avg_latency_ms: 200.0,
                avg_cost_cents: 5.0,
                avg_result_count: 8.0,
            },
            StrategyStats {
                strategy: "Fast".into(),
                sample_count: 100,
                success_rate: 0.30,
                avg_latency_ms: 10.0,
                avg_cost_cents: 1.0,
                avg_result_count: 2.0,
            },
        ];

        let best = SearchMetrics::pick_best_strategy(&stats).unwrap();
        assert_eq!(best.strategy, "Slow");
    }
}
