// KG Persistence Layer
// Phase 5.5: Persist execution history to SurrealDB for durability and
// analytics

use std::sync::Arc;

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use surrealdb::engine::remote::ws::Client;
use surrealdb::Surreal;
use tracing::debug;

use crate::learning::ExecutionRecord as LearningExecutionRecord;
use crate::metrics::{AnalyticsComputation, TimePeriod};
use crate::models::ExecutionRecord;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedExecution {
    pub execution_id: String,
    pub task_description: String,
    pub agent_id: String,
    pub agent_role: Option<String>,
    pub provider: String,
    pub cost_cents: u32,
    pub outcome: String,
    pub duration_ms: u64,
    pub input_tokens: u64,
    pub output_tokens: u64,
    pub task_type: String,
    pub error_message: Option<String>,
    pub solution: Option<String>,
    pub embedding: Vec<f32>,
    pub executed_at: String,
    pub created_at: String,
}

impl PersistedExecution {
    /// Convert from ExecutionRecord and embedding
    pub fn from_execution_record(record: &ExecutionRecord, embedding: Vec<f32>) -> Self {
        Self {
            execution_id: record.id.clone(),
            task_description: record.description.clone(),
            agent_id: record.agent_id.clone(),
            agent_role: record.agent_role.clone(),
            provider: record.provider.clone(),
            cost_cents: record.cost_cents,
            outcome: if record.success {
                "success".to_string()
            } else {
                "failure".to_string()
            },
            duration_ms: record.duration_ms,
            input_tokens: record.input_tokens,
            output_tokens: record.output_tokens,
            task_type: record.task_type.clone(),
            error_message: record.error.clone(),
            solution: record.solution.clone(),
            embedding,
            executed_at: record.timestamp.to_rfc3339(),
            created_at: Utc::now().to_rfc3339(),
        }
    }
}

/// RLM execution record for distributed reasoning tasks
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedRlmExecution {
    pub execution_id: String,
    pub doc_id: String,
    pub query: String,
    pub chunks_used: Vec<String>,
    pub result: Option<String>,
    pub duration_ms: u64,
    pub cost_cents: f64,
    pub provider: Option<String>,
    pub success: bool,
    pub error_message: Option<String>,
    pub input_tokens: u64,
    pub output_tokens: u64,
    pub num_llm_calls: usize,
    pub aggregation_strategy: Option<String>,
    pub query_embedding: Option<Vec<f32>>,
    pub metadata: Option<serde_json::Value>,
    pub executed_at: String,
    pub created_at: String,
}

/// Builder for PersistedRlmExecution to avoid too many arguments
#[derive(Debug, Clone)]
pub struct RlmExecutionBuilder {
    execution_id: String,
    doc_id: String,
    query: String,
    chunks_used: Vec<String>,
    result: Option<String>,
    duration_ms: u64,
    input_tokens: u64,
    output_tokens: u64,
    num_llm_calls: usize,
    provider: Option<String>,
    success: bool,
    error_message: Option<String>,
    cost_cents: f64,
    aggregation_strategy: Option<String>,
    query_embedding: Option<Vec<f32>>,
    metadata: Option<serde_json::Value>,
}

impl RlmExecutionBuilder {
    /// Create new builder with required fields
    pub fn new(execution_id: String, doc_id: String, query: String) -> Self {
        Self {
            execution_id,
            doc_id,
            query,
            chunks_used: Vec::new(),
            result: None,
            duration_ms: 0,
            input_tokens: 0,
            output_tokens: 0,
            num_llm_calls: 1,
            provider: None,
            success: false,
            error_message: None,
            cost_cents: 0.0,
            aggregation_strategy: None,
            query_embedding: None,
            metadata: None,
        }
    }

    pub fn chunks_used(mut self, chunks: Vec<String>) -> Self {
        self.chunks_used = chunks;
        self
    }

    pub fn result(mut self, result: String) -> Self {
        self.result = Some(result);
        self
    }

    pub fn duration_ms(mut self, duration: u64) -> Self {
        self.duration_ms = duration;
        self
    }

    pub fn tokens(mut self, input: u64, output: u64) -> Self {
        self.input_tokens = input;
        self.output_tokens = output;
        self
    }

    pub fn num_llm_calls(mut self, calls: usize) -> Self {
        self.num_llm_calls = calls;
        self
    }

    pub fn provider(mut self, provider: String) -> Self {
        self.provider = Some(provider);
        self
    }

    pub fn success(mut self, success: bool) -> Self {
        self.success = success;
        self
    }

    pub fn error(mut self, error: String) -> Self {
        self.error_message = Some(error);
        self.success = false;
        self
    }

    pub fn cost_cents(mut self, cost: f64) -> Self {
        self.cost_cents = cost;
        self
    }

    pub fn aggregation_strategy(mut self, strategy: String) -> Self {
        self.aggregation_strategy = Some(strategy);
        self
    }

    pub fn query_embedding(mut self, embedding: Vec<f32>) -> Self {
        self.query_embedding = Some(embedding);
        self
    }

    pub fn metadata(mut self, metadata: serde_json::Value) -> Self {
        self.metadata = Some(metadata);
        self
    }

    pub fn build(self) -> PersistedRlmExecution {
        let now = Utc::now().to_rfc3339();
        PersistedRlmExecution {
            execution_id: self.execution_id,
            doc_id: self.doc_id,
            query: self.query,
            chunks_used: self.chunks_used,
            result: self.result,
            duration_ms: self.duration_ms,
            cost_cents: self.cost_cents,
            provider: self.provider,
            success: self.success,
            error_message: self.error_message,
            input_tokens: self.input_tokens,
            output_tokens: self.output_tokens,
            num_llm_calls: self.num_llm_calls,
            aggregation_strategy: self.aggregation_strategy,
            query_embedding: self.query_embedding,
            metadata: self.metadata,
            executed_at: now.clone(),
            created_at: now,
        }
    }
}

impl PersistedRlmExecution {
    /// Create new builder
    pub fn builder(execution_id: String, doc_id: String, query: String) -> RlmExecutionBuilder {
        RlmExecutionBuilder::new(execution_id, doc_id, query)
    }
}

/// Implement learning::ExecutionRecord trait for PersistedRlmExecution
impl LearningExecutionRecord for PersistedRlmExecution {
    fn timestamp(&self) -> DateTime<Utc> {
        chrono::DateTime::parse_from_rfc3339(&self.executed_at)
            .map(|dt| dt.with_timezone(&Utc))
            .unwrap_or_else(|_| Utc::now())
    }

    fn success(&self) -> bool {
        self.success
    }

    fn duration_ms(&self) -> u64 {
        self.duration_ms
    }
}

pub struct KGPersistence {
    db: Arc<Surreal<Client>>,
    analytics: Option<Arc<dyn AnalyticsComputation>>,
}

impl std::fmt::Debug for KGPersistence {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("KGPersistence")
            .field("db", &"<SurrealDB>")
            .field("analytics", &"<AnalyticsComputation>")
            .finish()
    }
}

impl KGPersistence {
    /// Create new persistence layer
    pub fn new(db: Surreal<Client>) -> Self {
        Self {
            db: Arc::new(db),
            analytics: None,
        }
    }

    /// Create with analytics computation provider
    pub fn with_analytics(db: Surreal<Client>, analytics: Arc<dyn AnalyticsComputation>) -> Self {
        Self {
            db: Arc::new(db),
            analytics: Some(analytics),
        }
    }

    /// Set analytics computation provider
    pub fn set_analytics(&mut self, analytics: Arc<dyn AnalyticsComputation>) {
        self.analytics = Some(analytics);
    }

    /// Persist a single execution record
    pub async fn persist_execution(&self, execution: PersistedExecution) -> anyhow::Result<()> {
        debug!(
            "Persisting execution {} for agent {}",
            execution.execution_id, execution.agent_id
        );

        let _: Option<serde_json::Value> = self
            .db
            .create::<Option<serde_json::Value>>("kg_executions")
            .content(serde_json::to_value(&execution)?)
            .await?;

        Ok(())
    }

    /// Persist multiple execution records (batch operation)
    pub async fn persist_executions(
        &self,
        executions: Vec<PersistedExecution>,
    ) -> anyhow::Result<()> {
        if executions.is_empty() {
            return Ok(());
        }

        debug!("Persisting {} executions in batch", executions.len());

        for execution in executions {
            self.persist_execution(execution).await?;
        }

        Ok(())
    }

    /// Find similar executions using SurrealDB 3 HNSW approximate nearest
    /// neighbor search.
    ///
    /// The `<|100,64|>` operator retrieves 100 HNSW candidates with ef=64
    /// search expansion, then reranks by exact cosine similarity. Falls
    /// back to recent successful executions when no embedding is provided.
    pub async fn find_similar_executions(
        &self,
        embedding: &[f32],
        limit: usize,
    ) -> anyhow::Result<Vec<PersistedExecution>> {
        debug!(
            "HNSW semantic search for similar executions (limit: {})",
            limit
        );

        if embedding.is_empty() {
            let query = format!(
                "SELECT * FROM kg_executions WHERE outcome = 'success' ORDER BY executed_at DESC \
                 LIMIT {limit}"
            );
            let mut response = self.db.query(&query).await?;
            let raw: Vec<serde_json::Value> = response.take(0)?;
            return Ok(raw
                .into_iter()
                .filter_map(|v| serde_json::from_value(v).ok())
                .collect());
        }

        let results = self
            .search_by_embedding_inner(embedding, limit)
            .await?
            .into_iter()
            .map(|(exec, _score)| exec)
            .collect();

        Ok(results)
    }

    /// Hybrid search combining HNSW vector similarity and BM25 full-text
    /// search, fused via Reciprocal Rank Fusion (RRF, k=60).
    ///
    /// Requires migration 012 (HNSW + BM25 indexes) to be applied.
    pub async fn hybrid_search(
        &self,
        embedding: &[f32],
        text_query: &str,
        limit: usize,
    ) -> anyhow::Result<Vec<crate::models::HybridSearchResult>> {
        use std::collections::HashMap;

        let candidate_limit = (limit * 3).max(30);

        let semantic_results = if !embedding.is_empty() {
            self.search_by_embedding_inner(embedding, candidate_limit)
                .await
                .unwrap_or_default()
        } else {
            Vec::new()
        };

        let lexical_results = if !text_query.trim().is_empty() {
            self.search_by_text_inner(text_query, candidate_limit)
                .await
                .unwrap_or_default()
        } else {
            Vec::new()
        };

        if semantic_results.is_empty() && lexical_results.is_empty() {
            return Ok(Vec::new());
        }

        let semantic_ranked: Vec<String> = semantic_results
            .iter()
            .map(|(e, _)| e.execution_id.clone())
            .collect();
        let lexical_ranked: Vec<String> = lexical_results
            .iter()
            .map(|(e, _)| e.execution_id.clone())
            .collect();

        let semantic_score_map: HashMap<String, f64> = semantic_results
            .iter()
            .map(|(e, s)| (e.execution_id.clone(), *s))
            .collect();
        let lexical_score_map: HashMap<String, f64> = lexical_results
            .iter()
            .map(|(e, s)| (e.execution_id.clone(), *s))
            .collect();

        // Merge execution records without double-fetching (prefer semantic)
        let mut exec_map: HashMap<String, PersistedExecution> = HashMap::new();
        for (exec, _) in semantic_results.into_iter().chain(lexical_results) {
            exec_map.entry(exec.execution_id.clone()).or_insert(exec);
        }

        // Collect unique IDs for RRF
        let mut all_ids: Vec<String> = Vec::with_capacity(exec_map.len());
        let mut seen = std::collections::HashSet::new();
        for id in semantic_ranked.iter().chain(lexical_ranked.iter()) {
            if seen.insert(id.clone()) {
                all_ids.push(id.clone());
            }
        }

        let mut fused: Vec<crate::models::HybridSearchResult> = all_ids
            .into_iter()
            .filter_map(|id| {
                // RRF: 1-indexed ranks; absent items get rank = list_len + 1
                let sem_rank = semantic_ranked
                    .iter()
                    .position(|x| x == &id)
                    .map_or(semantic_ranked.len() + 1, |r| r + 1);
                let lex_rank = lexical_ranked
                    .iter()
                    .position(|x| x == &id)
                    .map_or(lexical_ranked.len() + 1, |r| r + 1);

                const K: f64 = 60.0;
                let hybrid_score = 1.0 / (K + sem_rank as f64) + 1.0 / (K + lex_rank as f64);

                let execution = exec_map.remove(&id)?;

                Some(crate::models::HybridSearchResult {
                    semantic_score: semantic_score_map.get(&id).copied().unwrap_or(0.0),
                    lexical_score: lexical_score_map.get(&id).copied().unwrap_or(0.0),
                    hybrid_score,
                    semantic_rank: sem_rank,
                    lexical_rank: lex_rank,
                    execution,
                })
            })
            .collect();

        fused.sort_by(|a, b| {
            b.hybrid_score
                .partial_cmp(&a.hybrid_score)
                .unwrap_or(std::cmp::Ordering::Equal)
        });
        fused.truncate(limit);

        debug!(
            "Hybrid search returned {} results (semantic: {}, lexical: {} unique candidates)",
            fused.len(),
            semantic_ranked.len(),
            lexical_ranked.len()
        );

        Ok(fused)
    }

    /// HNSW approximate nearest neighbor search on `embedding` field.
    ///
    /// `<|100,64|>`: retrieve 100 candidates from HNSW index with ef=64.
    /// Results are reranked by exact cosine similarity before returning.
    async fn search_by_embedding_inner(
        &self,
        embedding: &[f32],
        limit: usize,
    ) -> anyhow::Result<Vec<(PersistedExecution, f64)>> {
        let mut response = self
            .db
            .query(
                "SELECT *, vector::similarity::cosine(embedding, $q) AS cosine_score FROM \
                 kg_executions WHERE embedding <|100,64|> $q ORDER BY cosine_score DESC",
            )
            .bind(("q", embedding.to_vec()))
            .await?;

        let raw: Vec<serde_json::Value> = response.take(0)?;
        let results = raw
            .into_iter()
            .filter_map(|mut v| {
                let score = v.get("cosine_score")?.as_f64()?;
                if let Some(obj) = v.as_object_mut() {
                    obj.remove("cosine_score");
                    obj.remove("id");
                }
                let exec: PersistedExecution = serde_json::from_value(v).ok()?;
                Some((exec, score))
            })
            .take(limit)
            .collect();

        Ok(results)
    }

    /// BM25 full-text search on `task_description` field.
    ///
    /// Requires `idx_kg_executions_ft` index (migration 012).
    /// The `@1@` predicate tag pairs with `search::score(1)` for BM25 scoring.
    async fn search_by_text_inner(
        &self,
        text_query: &str,
        limit: usize,
    ) -> anyhow::Result<Vec<(PersistedExecution, f64)>> {
        let mut response = self
            .db
            .query(
                "SELECT *, search::score(1) AS bm25_score FROM kg_executions WHERE \
                 task_description @1@ $text ORDER BY bm25_score DESC LIMIT 100",
            )
            .bind(("text", text_query.to_string()))
            .await?;

        let raw: Vec<serde_json::Value> = response.take(0)?;
        let results = raw
            .into_iter()
            .filter_map(|mut v| {
                let score = v.get("bm25_score")?.as_f64().unwrap_or(0.0);
                if let Some(obj) = v.as_object_mut() {
                    obj.remove("bm25_score");
                    obj.remove("id");
                }
                let exec: PersistedExecution = serde_json::from_value(v).ok()?;
                Some((exec, score))
            })
            .take(limit)
            .collect();

        Ok(results)
    }

    /// Get success rate by agent
    pub async fn get_agent_success_rate(&self, agent_id: &str) -> anyhow::Result<f64> {
        debug!("Fetching success rate for agent {}", agent_id);

        let query = format!(
            "SELECT count(SELECT outcome FROM kg_executions WHERE outcome = 'success' AND \
             agent_id = '{}') * 100.0 / count(SELECT * FROM kg_executions WHERE agent_id = '{}') \
             AS rate FROM kg_executions",
            agent_id, agent_id
        );

        let mut response = self.db.query(&query).await?;

        let result: Vec<serde_json::Value> = response.take(0)?;
        Ok(result
            .first()
            .and_then(|r| r["rate"].as_f64())
            .unwrap_or(0.0))
    }

    /// Get task type distribution
    pub async fn get_task_distribution(&self) -> anyhow::Result<Vec<(String, u64, f64)>> {
        debug!("Fetching task type distribution");

        let query = "SELECT task_type, count(*) as count FROM kg_executions GROUP BY task_type";

        let mut response = self.db.query(query).await?;

        let results: Vec<serde_json::Value> = response.take(0)?;
        Ok(results
            .into_iter()
            .filter_map(|r| {
                let task_type = r["task_type"].as_str()?.to_string();
                let count = r["count"].as_u64()?;
                Some((task_type, count, 0.0))
            })
            .collect())
    }

    /// Cleanup old executions (keep only last N days)
    pub async fn cleanup_old_executions(&self, days: i32) -> anyhow::Result<u64> {
        debug!("Cleaning up executions older than {} days", days);

        let cutoff = Utc::now() - chrono::Duration::days(days as i64);
        let cutoff_str = cutoff.to_rfc3339();

        let query = format!(
            "DELETE FROM kg_executions WHERE executed_at < '{}'",
            cutoff_str
        );

        let mut response = self.db.query(&query).await?;
        let _result: Vec<serde_json::Value> = response.take(0)?;
        Ok(0) // SurrealDB doesn't return delete count from DELETE queries
    }

    /// Get total execution count
    pub async fn get_execution_count(&self) -> anyhow::Result<u64> {
        debug!("Fetching execution count");

        let query = "SELECT count(*) as total FROM kg_executions";
        let mut response = self.db.query(query).await?;
        let result: Vec<serde_json::Value> = response.take(0)?;
        Ok(result
            .first()
            .and_then(|r| r["total"].as_u64())
            .unwrap_or(0))
    }

    /// Get task-type specific executions for agent (for learning profiles).
    /// Returns executions filtered by agent_id and task_type, limited to recent
    /// data.
    pub async fn get_executions_for_task_type(
        &self,
        agent_id: &str,
        task_type: &str,
        limit: usize,
    ) -> anyhow::Result<Vec<PersistedExecution>> {
        debug!(
            "Fetching executions for agent {} task_type {} (limit: {})",
            agent_id, task_type, limit
        );

        let query = format!(
            "SELECT * FROM kg_executions WHERE agent_id = '{}' AND task_type = '{}' ORDER BY \
             executed_at DESC LIMIT {}",
            agent_id, task_type, limit
        );

        let mut response = self.db.query(&query).await?;
        let raw: Vec<serde_json::Value> = response.take(0)?;
        Ok(raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect())
    }

    /// Get all recent executions for agent across all task types.
    /// Useful for computing overall success rate and expertise.
    pub async fn get_agent_executions(
        &self,
        agent_id: &str,
        limit: usize,
    ) -> anyhow::Result<Vec<PersistedExecution>> {
        debug!(
            "Fetching all executions for agent {} (limit: {})",
            agent_id, limit
        );

        let query = format!(
            "SELECT * FROM kg_executions WHERE agent_id = '{}' ORDER BY executed_at DESC LIMIT {}",
            agent_id, limit
        );

        let mut response = self.db.query(&query).await?;
        let raw: Vec<serde_json::Value> = response.take(0)?;
        Ok(raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect())
    }

    /// Record analytics event
    pub async fn record_event(
        &self,
        event_type: &str,
        agent_id: &str,
        metric_name: &str,
        metric_value: f64,
    ) -> anyhow::Result<()> {
        debug!(
            "Recording event: {} for agent {} ({}={})",
            event_type, agent_id, metric_name, metric_value
        );

        let event_id = uuid::Uuid::new_v4().to_string();
        let now = Utc::now().to_rfc3339();

        let event = serde_json::json!({
            "event_id": event_id,
            "event_type": event_type,
            "agent_id": agent_id,
            "metric_name": metric_name,
            "metric_value": metric_value,
            "recorded_at": now,
            "created_at": now,
        });

        let _: Option<serde_json::Value> = self
            .db
            .create::<Option<serde_json::Value>>("analytics_events")
            .content(event)
            .await?;

        Ok(())
    }

    /// Get agent performance metrics for a specific time period
    pub async fn get_agent_performance(
        &self,
        agent_id: &str,
        period: TimePeriod,
    ) -> anyhow::Result<crate::analytics::AgentPerformance> {
        // Fetch executions for agent in the given period
        let executions = self.get_agent_executions(agent_id, 1000).await?;

        // Filter by time period
        let cutoff = Utc::now() - period.duration();
        let filtered: Vec<PersistedExecution> = executions
            .into_iter()
            .filter(|e| {
                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&e.executed_at) {
                    dt.with_timezone(&Utc) > cutoff
                } else {
                    false
                }
            })
            .collect();

        if let Some(analytics) = &self.analytics {
            analytics
                .compute_agent_performance(filtered, period, agent_id)
                .await
        } else {
            anyhow::bail!("Analytics computation provider not set")
        }
    }

    /// Get task type analytics across all agents
    pub async fn get_task_type_analytics(
        &self,
        task_type: &str,
        period: TimePeriod,
    ) -> anyhow::Result<crate::analytics::TaskTypeAnalytics> {
        // Fetch executions for task type
        let query = format!(
            "SELECT * FROM kg_executions WHERE task_type = '{}' ORDER BY executed_at DESC LIMIT \
             1000",
            task_type
        );

        let mut response = self.db.query(&query).await?;
        let raw: Vec<serde_json::Value> = response.take(0)?;
        let executions: Vec<PersistedExecution> = raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect();

        // Filter by time period
        let cutoff = Utc::now() - period.duration();
        let filtered: Vec<PersistedExecution> = executions
            .into_iter()
            .filter(|e| {
                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&e.executed_at) {
                    dt.with_timezone(&Utc) > cutoff
                } else {
                    false
                }
            })
            .collect();

        if let Some(analytics) = &self.analytics {
            analytics
                .compute_task_type_analytics(filtered, task_type)
                .await
        } else {
            anyhow::bail!("Analytics computation provider not set")
        }
    }

    /// Get comprehensive dashboard metrics for the system
    pub async fn get_dashboard_metrics(
        &self,
        period: TimePeriod,
    ) -> anyhow::Result<crate::analytics::DashboardMetrics> {
        // Fetch all recent executions
        let query = "SELECT * FROM kg_executions ORDER BY executed_at DESC LIMIT 5000";
        let mut response = self.db.query(query).await?;
        let raw: Vec<serde_json::Value> = response.take(0)?;
        let executions: Vec<PersistedExecution> = raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect();

        // Filter by time period
        let cutoff = Utc::now() - period.duration();
        let filtered: Vec<PersistedExecution> = executions
            .into_iter()
            .filter(|e| {
                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&e.executed_at) {
                    dt.with_timezone(&Utc) > cutoff
                } else {
                    false
                }
            })
            .collect();

        if let Some(analytics) = &self.analytics {
            analytics.compute_dashboard_metrics(filtered, period).await
        } else {
            anyhow::bail!("Analytics computation provider not set")
        }
    }

    /// Get cost efficiency report for the period
    pub async fn get_cost_report(
        &self,
        period: TimePeriod,
    ) -> anyhow::Result<crate::analytics::CostEfficiencyReport> {
        // Fetch all recent executions
        let query = "SELECT * FROM kg_executions ORDER BY executed_at DESC LIMIT 5000";
        let mut response = self.db.query(query).await?;
        let raw: Vec<serde_json::Value> = response.take(0)?;
        let executions: Vec<PersistedExecution> = raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect();

        // Filter by time period
        let cutoff = Utc::now() - period.duration();
        let filtered: Vec<PersistedExecution> = executions
            .into_iter()
            .filter(|e| {
                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&e.executed_at) {
                    dt.with_timezone(&Utc) > cutoff
                } else {
                    false
                }
            })
            .collect();

        if let Some(analytics) = &self.analytics {
            analytics.compute_cost_report(filtered, period).await
        } else {
            anyhow::bail!("Analytics computation provider not set")
        }
    }

    // ========================================================================
    // RLM-Specific Methods (Phase 7)
    // ========================================================================

    /// Persist a single RLM execution record
    pub async fn persist_rlm_execution(
        &self,
        execution: PersistedRlmExecution,
    ) -> anyhow::Result<()> {
        debug!(
            "Persisting RLM execution {} for doc {}",
            execution.execution_id, execution.doc_id
        );

        // Use SQL query with parameterized bindings for SurrealDB 2.6 compatibility
        let query = "CREATE rlm_executions SET execution_id = $execution_id, doc_id = $doc_id, \
                     query = $query, chunks_used = $chunks_used, result = $result, duration_ms = \
                     $duration_ms, cost_cents = $cost_cents, provider = $provider, success = \
                     $success, error_message = $error_message, input_tokens = $input_tokens, \
                     output_tokens = $output_tokens, num_llm_calls = $num_llm_calls, \
                     aggregation_strategy = $aggregation_strategy, query_embedding = \
                     $query_embedding, metadata = $metadata, executed_at = $executed_at, \
                     created_at = $created_at";

        let response = self
            .db
            .query(query)
            .bind(("execution_id", execution.execution_id))
            .bind(("doc_id", execution.doc_id))
            .bind(("query", execution.query))
            .bind(("chunks_used", execution.chunks_used))
            .bind(("result", execution.result))
            .bind(("duration_ms", execution.duration_ms as i64))
            .bind(("cost_cents", execution.cost_cents))
            .bind(("provider", execution.provider))
            .bind(("success", execution.success))
            .bind(("error_message", execution.error_message))
            .bind(("input_tokens", execution.input_tokens as i64))
            .bind(("output_tokens", execution.output_tokens as i64))
            .bind(("num_llm_calls", execution.num_llm_calls as i64))
            .bind(("aggregation_strategy", execution.aggregation_strategy))
            .bind(("query_embedding", execution.query_embedding))
            .bind(("metadata", execution.metadata))
            .bind(("executed_at", execution.executed_at))
            .bind(("created_at", execution.created_at))
            .await?;

        // Check for errors
        response.check()?;

        Ok(())
    }

    /// Persist multiple RLM execution records (batch operation)
    pub async fn persist_rlm_executions(
        &self,
        executions: Vec<PersistedRlmExecution>,
    ) -> anyhow::Result<()> {
        if executions.is_empty() {
            return Ok(());
        }

        debug!("Persisting {} RLM executions in batch", executions.len());

        for execution in executions {
            self.persist_rlm_execution(execution).await?;
        }

        Ok(())
    }

    /// Get RLM learning curve for a specific document
    /// Returns time-series of success rates grouped by time windows
    pub async fn get_rlm_learning_curve(
        &self,
        doc_id: &str,
        window_days: u32,
    ) -> anyhow::Result<Vec<(DateTime<Utc>, f64)>> {
        debug!(
            "Computing RLM learning curve for doc {} (window: {} days)",
            doc_id, window_days
        );

        // Fetch all executions for this document
        let executions = self.get_rlm_executions_by_doc(doc_id, 1000).await?;

        if executions.is_empty() {
            return Ok(Vec::new());
        }

        // Use existing learning curve calculation
        let curve = crate::learning::calculate_learning_curve(executions, window_days);
        Ok(curve)
    }

    /// Get RLM executions for a specific document
    pub async fn get_rlm_executions_by_doc(
        &self,
        doc_id: &str,
        limit: usize,
    ) -> anyhow::Result<Vec<PersistedRlmExecution>> {
        debug!(
            "Fetching RLM executions for doc {} (limit: {})",
            doc_id, limit
        );

        let query = format!(
            "SELECT * FROM rlm_executions WHERE doc_id = '{}' ORDER BY executed_at DESC LIMIT {}",
            doc_id, limit
        );

        let mut response = self.db.query(&query).await?;
        let raw: Vec<serde_json::Value> = response.take(0)?;
        Ok(raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect())
    }

    /// Find similar RLM tasks using cosine similarity on `query_embedding`.
    ///
    /// `rlm_executions` is SCHEMALESS with nullable `query_embedding`, making
    /// HNSW indexing impractical. In-memory cosine similarity is used instead
    /// since RLM datasets are per-document and bounded in size.
    pub async fn find_similar_rlm_tasks(
        &self,
        query_embedding: &[f32],
        limit: usize,
    ) -> anyhow::Result<Vec<PersistedRlmExecution>> {
        debug!(
            "Cosine similarity search for similar RLM tasks (limit: {})",
            limit
        );

        let candidate_limit = (limit * 10).max(200);
        let query = format!(
            "SELECT * FROM rlm_executions WHERE success = true ORDER BY executed_at DESC LIMIT \
             {candidate_limit}"
        );
        let mut response = self.db.query(&query).await?;
        let raw: Vec<serde_json::Value> = response.take(0)?;
        let candidates: Vec<PersistedRlmExecution> = raw
            .into_iter()
            .filter_map(|v| serde_json::from_value(v).ok())
            .collect();

        if query_embedding.is_empty() || candidates.is_empty() {
            return Ok(candidates.into_iter().take(limit).collect());
        }

        let mut scored: Vec<(PersistedRlmExecution, f64)> = candidates
            .into_iter()
            .filter_map(|exec| {
                let emb = exec.query_embedding.as_deref()?;
                let sim = cosine_similarity(query_embedding, emb);
                Some((exec, sim))
            })
            .collect();

        scored.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));

        Ok(scored.into_iter().take(limit).map(|(e, _)| e).collect())
    }

    /// Get RLM success rate for a specific document
    pub async fn get_rlm_success_rate(&self, doc_id: &str) -> anyhow::Result<f64> {
        debug!("Fetching RLM success rate for doc {}", doc_id);

        let executions = self.get_rlm_executions_by_doc(doc_id, 1000).await?;

        if executions.is_empty() {
            return Ok(0.5);
        }

        let total = executions.len() as f64;
        let successes = executions.iter().filter(|e| e.success).count() as f64;
        Ok(successes / total)
    }

    /// Get RLM cost summary for a document over a time period
    pub async fn get_rlm_cost_summary(
        &self,
        doc_id: &str,
        period: TimePeriod,
    ) -> anyhow::Result<(f64, u64, u64)> {
        debug!(
            "Computing RLM cost summary for doc {} ({:?})",
            doc_id, period
        );

        let executions = self.get_rlm_executions_by_doc(doc_id, 5000).await?;

        // Filter by time period
        let cutoff = Utc::now() - period.duration();
        let filtered: Vec<PersistedRlmExecution> = executions
            .into_iter()
            .filter(|e| {
                if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&e.executed_at) {
                    dt.with_timezone(&Utc) > cutoff
                } else {
                    false
                }
            })
            .collect();

        if filtered.is_empty() {
            return Ok((0.0, 0, 0));
        }

        let total_cost: f64 = filtered.iter().map(|e| e.cost_cents).sum();
        let total_input_tokens: u64 = filtered.iter().map(|e| e.input_tokens).sum();
        let total_output_tokens: u64 = filtered.iter().map(|e| e.output_tokens).sum();

        Ok((total_cost, total_input_tokens, total_output_tokens))
    }

    /// Get total RLM execution count
    pub async fn get_rlm_execution_count(&self) -> anyhow::Result<u64> {
        debug!("Fetching RLM execution count");

        let query = "SELECT count() as total FROM rlm_executions GROUP ALL";
        let mut response = self.db.query(query).await?;
        let result: Vec<serde_json::Value> = response.take(0)?;
        Ok(result
            .first()
            .and_then(|r| r["total"].as_u64())
            .unwrap_or(0))
    }

    /// Cleanup old RLM executions (keep only last N days)
    pub async fn cleanup_old_rlm_executions(&self, days: i32) -> anyhow::Result<u64> {
        debug!("Cleaning up RLM executions older than {} days", days);

        let cutoff = Utc::now() - chrono::Duration::days(days as i64);
        let cutoff_str = cutoff.to_rfc3339();

        let query = format!(
            "DELETE FROM rlm_executions WHERE executed_at < '{}'",
            cutoff_str
        );

        let mut response = self.db.query(&query).await?;
        let _: Vec<serde_json::Value> = response.take(0)?;
        Ok(0) // SurrealDB DELETE doesn't return a count
    }
}

/// Cosine similarity between two equal-length f32 vectors.
/// Returns 0.0 for empty or mismatched-dimension inputs.
fn cosine_similarity(a: &[f32], b: &[f32]) -> f64 {
    if a.len() != b.len() || a.is_empty() {
        return 0.0;
    }
    let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
    let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
    let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
    if norm_a > 0.0 && norm_b > 0.0 {
        (dot / (norm_a * norm_b)) as f64
    } else {
        0.0
    }
}

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

    #[test]
    fn test_persisted_execution_creation() {
        let record = ExecutionRecord {
            id: "exec-1".to_string(),
            task_id: "task-1".to_string(),
            agent_id: "agent-1".to_string(),
            agent_role: None,
            task_type: "coding".to_string(),
            description: "Write code".to_string(),
            duration_ms: 5000,
            input_tokens: 100,
            output_tokens: 250,
            cost_cents: 75,
            provider: "claude".to_string(),
            success: true,
            error: None,
            solution: Some("Use async/await".to_string()),
            root_cause: None,
            timestamp: Utc::now(),
        };

        let embedding = vec![0.1; 1536];
        let persisted = PersistedExecution::from_execution_record(&record, embedding.clone());

        assert_eq!(persisted.execution_id, "exec-1");
        assert_eq!(persisted.agent_id, "agent-1");
        assert_eq!(persisted.outcome, "success");
        assert_eq!(persisted.embedding.len(), 1536);
    }

    #[test]
    fn test_persisted_rlm_execution_creation() {
        let execution = PersistedRlmExecution::builder(
            "rlm-exec-1".to_string(),
            "doc-1".to_string(),
            "What is Rust ownership?".to_string(),
        )
        .chunks_used(vec!["chunk-1".to_string(), "chunk-2".to_string()])
        .result("Rust ownership system ensures memory safety".to_string())
        .duration_ms(5000)
        .tokens(1000, 500)
        .num_llm_calls(3)
        .provider("claude".to_string())
        .success(true)
        .build();

        assert_eq!(execution.execution_id, "rlm-exec-1");
        assert_eq!(execution.doc_id, "doc-1");
        assert_eq!(execution.chunks_used.len(), 2);
        assert_eq!(execution.input_tokens, 1000);
        assert_eq!(execution.output_tokens, 500);
        assert_eq!(execution.num_llm_calls, 3);
        assert!(execution.success);
    }

    #[test]
    fn test_persisted_rlm_execution_with_builders() {
        let execution = PersistedRlmExecution::builder(
            "rlm-exec-2".to_string(),
            "doc-2".to_string(),
            "Explain lifetimes".to_string(),
        )
        .chunks_used(vec!["chunk-1".to_string()])
        .result("Lifetimes track scope".to_string())
        .duration_ms(3000)
        .tokens(800, 400)
        .provider("gpt-4".to_string())
        .success(true)
        .cost_cents(150.0)
        .aggregation_strategy("Concatenate".to_string())
        .query_embedding(vec![0.1; 1536])
        .metadata(serde_json::json!({"key": "value"}))
        .build();

        assert_eq!(execution.cost_cents, 150.0);
        assert_eq!(
            execution.aggregation_strategy,
            Some("Concatenate".to_string())
        );
        assert_eq!(execution.query_embedding.as_ref().unwrap().len(), 1536);
        assert!(execution.metadata.is_some());
    }

    #[test]
    fn test_rlm_execution_implements_learning_trait() {
        let execution = PersistedRlmExecution::builder(
            "rlm-exec-3".to_string(),
            "doc-3".to_string(),
            "Test query".to_string(),
        )
        .duration_ms(1000)
        .tokens(100, 50)
        .success(true)
        .build();

        // Test trait methods
        let timestamp = execution.timestamp();
        assert!(timestamp <= Utc::now());
        assert_eq!(execution.success(), true);
        assert_eq!(execution.duration_ms(), 1000);
    }

    #[test]
    fn test_cosine_similarity_orthogonal() {
        let a = vec![1.0_f32, 0.0, 0.0];
        let b = vec![0.0_f32, 1.0, 0.0];
        assert!((cosine_similarity(&a, &b) - 0.0).abs() < 1e-6);
    }

    #[test]
    fn test_cosine_similarity_identical() {
        let v = vec![0.3_f32, 0.4, 0.5];
        let sim = cosine_similarity(&v, &v);
        assert!((sim - 1.0).abs() < 1e-5, "identical vectors: {sim}");
    }

    #[test]
    fn test_cosine_similarity_empty() {
        assert_eq!(cosine_similarity(&[], &[]), 0.0);
        assert_eq!(cosine_similarity(&[1.0], &[]), 0.0);
    }

    #[test]
    fn test_cosine_similarity_partial() {
        // [1,0] · [0.5, 0.866] ≈ cos(60°) ≈ 0.5
        let a = vec![1.0_f32, 0.0];
        let b = vec![0.5_f32, 0.866];
        let sim = cosine_similarity(&a, &b);
        assert!((sim - 0.5).abs() < 0.01, "got {sim}");
    }

    #[test]
    fn test_rrf_fusion_consensus_wins() {
        // Simulate the RRF logic: IDs in both lists should score higher
        const K: f64 = 60.0;
        let semantic = vec!["exec-a".to_string(), "exec-b".to_string()];
        let lexical = vec!["exec-b".to_string(), "exec-c".to_string()];

        let score_for = |id: &str| {
            let sem_rank = semantic
                .iter()
                .position(|x| x == id)
                .map_or(semantic.len() + 1, |r| r + 1);
            let lex_rank = lexical
                .iter()
                .position(|x| x == id)
                .map_or(lexical.len() + 1, |r| r + 1);
            1.0 / (K + sem_rank as f64) + 1.0 / (K + lex_rank as f64)
        };

        let score_b = score_for("exec-b");
        let score_a = score_for("exec-a");
        let score_c = score_for("exec-c");

        // exec-b appears in both lists, should rank highest
        assert!(
            score_b > score_a,
            "exec-b ({score_b}) should beat exec-a ({score_a})"
        );
        assert!(
            score_b > score_c,
            "exec-b ({score_b}) should beat exec-c ({score_c})"
        );
    }
}
