//! Query observability and evaluation engine.
//!
//! Combines validation, linting, safety checking, and complexity analysis
//! into a single evaluation pass, producing a quality scorecard, letter grade,
//! and structured observability events.

use std::collections::HashMap;
use std::time::Instant;

use crate::explainer::engine::explain;
use crate::linter::engine::lint;
use crate::linter::types::LintSeverity;
use crate::schema::types::SchemaDefinition;
use crate::validator::engine::validate;

use super::types::*;

/// Evaluate a SQL query across all quality dimensions.
///
/// This is the primary entry point for observability. It runs:
/// 1. Validation (syntax + semantic checks)
/// 2. Lint analysis (style and anti-pattern checks)
/// 3. Safety/policy evaluation (via guardrails)
/// 4. Complexity analysis (via the explainer)
///
/// Returns an [`EvalResult`] with scores, grade, and raw results.
///
/// # Example
///
/// ```rust,no_run
/// use altimate_core::observability::engine::evaluate;
/// use altimate_core::observability::types::EvalConfig;
/// use altimate_core::SchemaDefinition;
///
/// # async fn example() {
/// let schema = SchemaDefinition::from_yaml_file("schema.yaml").unwrap();
/// let result = evaluate("SELECT * FROM users", &schema, &EvalConfig::default()).await;
/// println!("Grade: {}", result.overall_grade);
/// println!("Score: {:.2}", result.scores.overall);
/// # }
/// ```
pub async fn evaluate(sql: &str, schema: &SchemaDefinition, config: &EvalConfig) -> EvalResult {
    let start = Instant::now();
    let mut scorecard = QualityScorecard::default();

    // 1. Validation
    let validation_result = if config.enable_validation {
        let result = validate(sql, schema).await;
        scorecard.syntax = if result.valid { 1.0 } else { 0.0 };
        Some(result)
    } else {
        scorecard.syntax = 1.0; // Assume valid when skipped
        None
    };
    let validation_json = validation_result
        .as_ref()
        .and_then(|r| serde_json::to_value(r).ok());

    // 2. Lint
    let lint_result = if config.enable_lint {
        let result = lint(sql, schema);
        scorecard.style = compute_lint_score(&result);
        Some(result)
    } else {
        scorecard.style = 1.0;
        None
    };
    let lint_json = lint_result
        .as_ref()
        .and_then(|r| serde_json::to_value(r).ok());

    // 3. Safety — use guardrails/policy engine if available
    // For now, compute a safety score from SQL heuristics since we don't
    // have a PolicyConfig passed in. In production, the caller would provide
    // a policy config for full safety scanning.
    scorecard.safety = compute_safety_score(sql);
    let safety_json: Option<serde_json::Value> = Some(serde_json::json!({
        "score": scorecard.safety,
        "method": "heuristic",
    }));

    // 4. Explain / complexity
    let explain_json = if config.enable_explain {
        let result = explain(sql, schema).await;
        if let Some(ref explanation) = result.explanation {
            scorecard.complexity = compute_complexity_from_signals(&explanation.cost_signals);
        } else {
            // If explain failed (invalid SQL), use heuristic
            scorecard.complexity = compute_complexity_heuristic(sql);
        }
        serde_json::to_value(&result).ok()
    } else {
        scorecard.complexity = 1.0;
        None
    };

    scorecard.compute_overall();
    let grade = Grade::from_score(scorecard.overall);
    let elapsed = start.elapsed().as_millis() as u64;

    EvalResult {
        sql: sql.to_string(),
        scores: scorecard,
        validation: validation_json,
        lint: lint_json,
        safety: safety_json,
        explain: explain_json,
        overall_grade: grade,
        total_time_ms: elapsed,
    }
}

/// Create a structured observability event from an evaluation result.
///
/// Produces an event compatible with observability platforms like
/// Langfuse and LangSmith.
pub fn create_event(sql: &str, result: &EvalResult) -> ObservabilityEvent {
    let error_count = result
        .validation
        .as_ref()
        .and_then(|v| v.get("errors"))
        .and_then(|e| e.as_array())
        .map(|a| a.len())
        .unwrap_or(0);

    let warning_count = result
        .lint
        .as_ref()
        .and_then(|v| v.get("warning_count"))
        .and_then(|w| w.as_u64())
        .unwrap_or(0) as usize;

    let timestamp = chrono::Utc::now().to_rfc3339();

    let mut metadata = HashMap::new();
    metadata.insert(
        "syntax_valid".to_string(),
        serde_json::Value::Bool(result.scores.syntax >= 1.0),
    );
    metadata.insert(
        "grade".to_string(),
        serde_json::Value::String(result.overall_grade.to_string()),
    );

    ObservabilityEvent {
        event_type: "sql_evaluation".to_string(),
        timestamp,
        sql: sql.to_string(),
        scores: result.scores.clone(),
        grade: result.overall_grade,
        error_count,
        warning_count,
        duration_ms: result.total_time_ms,
        metadata,
    }
}

/// Evaluate a SQL query with default configuration.
///
/// Convenience wrapper around [`evaluate`] with [`EvalConfig::default()`].
pub async fn evaluate_default(sql: &str, schema: &SchemaDefinition) -> EvalResult {
    evaluate(sql, schema, &EvalConfig::default()).await
}

/// Compute a lint-based style score.
fn compute_lint_score(result: &crate::linter::types::LintResult) -> f64 {
    let mut score = 1.0_f64;
    for finding in &result.findings {
        match finding.severity {
            LintSeverity::Error => score -= 0.2,
            LintSeverity::Warning => score -= 0.1,
            LintSeverity::Info => score -= 0.05,
        }
    }
    score.max(0.0)
}

/// Compute a safety score from SQL heuristics.
///
/// Detects potentially dangerous patterns without a full policy engine.
fn compute_safety_score(sql: &str) -> f64 {
    let upper = sql.to_uppercase();
    let mut score = 1.0_f64;

    // Dangerous statement types
    if upper.contains("DROP ") {
        score -= 0.5;
    }
    if upper.contains("TRUNCATE ") {
        score -= 0.4;
    }
    if upper.contains("DELETE ") && !upper.contains("WHERE") {
        score -= 0.3;
    }
    if upper.contains("UPDATE ") && !upper.contains("WHERE") {
        score -= 0.3;
    }
    // Privilege escalation
    if upper.contains("GRANT ") || upper.contains("REVOKE ") {
        score -= 0.3;
    }
    // Data exfiltration patterns
    if upper.contains("INTO OUTFILE") || upper.contains("INTO DUMPFILE") {
        score -= 0.4;
    }

    score.max(0.0)
}

/// Compute complexity score from explainer cost signals.
fn compute_complexity_from_signals(signals: &crate::explainer::types::CostSignals) -> f64 {
    // Map the complexity score (0..=15+) to 0.0..=1.0 range
    // Score 0 → 1.0 (simple), Score 10+ → 0.0 (extreme)
    let raw = signals.complexity_score as f64;
    (1.0 - raw / 10.0).clamp(0.0, 1.0)
}

/// Compute complexity heuristic from SQL text when explainer is unavailable.
fn compute_complexity_heuristic(sql: &str) -> f64 {
    let upper = sql.to_uppercase();
    let mut penalty = 0.0_f64;

    let join_count = upper.matches(" JOIN ").count();
    penalty += join_count as f64 * 0.1;

    let subquery_count = upper.matches("SELECT").count();
    if subquery_count > 1 {
        penalty += (subquery_count - 1) as f64 * 0.15;
    }

    if upper.contains(" OVER(") || upper.contains(" OVER (") {
        penalty += 0.1;
    }

    if upper.contains(" UNION ") {
        penalty += 0.1;
    }

    if upper.contains("CROSS JOIN") {
        penalty += 0.2;
    }

    (1.0 - penalty).max(0.0)
}

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

    fn ecommerce_schema() -> SchemaDefinition {
        SchemaDefinition::from_yaml(include_str!(
            "../../../../tests/fixtures/schemas/ecommerce.yaml"
        ))
        .expect("Failed to load ecommerce schema fixture")
    }

    // ─── Basic evaluation ────────────────────────────────────

    #[tokio::test]
    async fn test_evaluate_valid_simple_query() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECT id FROM users", &schema).await;
        assert!(result.scores.syntax >= 1.0);
        assert_eq!(result.overall_grade, Grade::A);
    }

    #[tokio::test]
    async fn test_evaluate_valid_select_star() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECT * FROM users", &schema).await;
        assert!(result.scores.syntax >= 1.0);
        // SELECT * triggers lint warnings, so grade may be less than A
    }

    #[tokio::test]
    async fn test_evaluate_invalid_sql() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECTZ * FROM users", &schema).await;
        assert!(result.scores.syntax < 1.0);
        assert_ne!(result.overall_grade, Grade::A);
    }

    #[tokio::test]
    async fn test_evaluate_empty_sql() {
        let schema = ecommerce_schema();
        let result = evaluate_default("", &schema).await;
        assert!(result.scores.syntax < 1.0);
    }

    #[tokio::test]
    async fn test_evaluate_join_query() {
        let schema = ecommerce_schema();
        let sql = "SELECT u.id FROM users u JOIN orders o ON u.id = o.user_id";
        let result = evaluate_default(sql, &schema).await;
        assert!(result.scores.syntax >= 1.0);
        assert!(result.scores.complexity < 1.0); // Join adds complexity
    }

    // ─── Grade assignment ────────────────────────────────────

    #[test]
    fn test_grade_from_score_a() {
        assert_eq!(Grade::from_score(0.95), Grade::A);
        assert_eq!(Grade::from_score(0.9), Grade::A);
        assert_eq!(Grade::from_score(1.0), Grade::A);
    }

    #[test]
    fn test_grade_from_score_b() {
        assert_eq!(Grade::from_score(0.85), Grade::B);
        assert_eq!(Grade::from_score(0.8), Grade::B);
    }

    #[test]
    fn test_grade_from_score_c() {
        assert_eq!(Grade::from_score(0.75), Grade::C);
        assert_eq!(Grade::from_score(0.7), Grade::C);
    }

    #[test]
    fn test_grade_from_score_d() {
        assert_eq!(Grade::from_score(0.6), Grade::D);
        assert_eq!(Grade::from_score(0.5), Grade::D);
    }

    #[test]
    fn test_grade_from_score_f() {
        assert_eq!(Grade::from_score(0.49), Grade::F);
        assert_eq!(Grade::from_score(0.0), Grade::F);
    }

    #[test]
    fn test_grade_display() {
        assert_eq!(Grade::A.to_string(), "A");
        assert_eq!(Grade::B.to_string(), "B");
        assert_eq!(Grade::C.to_string(), "C");
        assert_eq!(Grade::D.to_string(), "D");
        assert_eq!(Grade::F.to_string(), "F");
    }

    // ─── Scorecard computation ───────────────────────────────

    #[test]
    fn test_scorecard_compute_overall_perfect() {
        let mut s = QualityScorecard {
            syntax: 1.0,
            style: 1.0,
            safety: 1.0,
            complexity: 1.0,
            overall: 0.0,
        };
        s.compute_overall();
        assert!((s.overall - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_scorecard_compute_overall_zero() {
        let mut s = QualityScorecard {
            syntax: 0.0,
            style: 0.0,
            safety: 0.0,
            complexity: 0.0,
            overall: 0.0,
        };
        s.compute_overall();
        assert!((s.overall - 0.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_scorecard_compute_overall_mixed() {
        let mut s = QualityScorecard {
            syntax: 1.0,
            style: 0.5,
            safety: 0.8,
            complexity: 0.6,
            overall: 0.0,
        };
        s.compute_overall();
        // 1.0*0.3 + 0.5*0.2 + 0.8*0.2 + 0.6*0.3 = 0.3 + 0.1 + 0.16 + 0.18 = 0.74
        assert!((s.overall - 0.74).abs() < 0.001);
    }

    // ─── Config variants ─────────────────────────────────────

    #[tokio::test]
    async fn test_evaluate_validation_disabled() {
        let schema = ecommerce_schema();
        let config = EvalConfig {
            enable_validation: false,
            ..Default::default()
        };
        let result = evaluate("SELECTZ broken", &schema, &config).await;
        // Validation disabled → syntax assumed valid
        assert!((result.scores.syntax - 1.0).abs() < f64::EPSILON);
        assert!(result.validation.is_none());
    }

    #[tokio::test]
    async fn test_evaluate_lint_disabled() {
        let schema = ecommerce_schema();
        let config = EvalConfig {
            enable_lint: false,
            ..Default::default()
        };
        let result = evaluate("SELECT * FROM users", &schema, &config).await;
        assert!((result.scores.style - 1.0).abs() < f64::EPSILON);
        assert!(result.lint.is_none());
    }

    #[tokio::test]
    async fn test_evaluate_explain_disabled() {
        let schema = ecommerce_schema();
        let config = EvalConfig {
            enable_explain: false,
            ..Default::default()
        };
        let result = evaluate("SELECT id FROM users", &schema, &config).await;
        assert!((result.scores.complexity - 1.0).abs() < f64::EPSILON);
        assert!(result.explain.is_none());
    }

    #[tokio::test]
    async fn test_evaluate_all_disabled() {
        let schema = ecommerce_schema();
        let config = EvalConfig {
            enable_validation: false,
            enable_lint: false,
            enable_safety: false,
            enable_explain: false,
        };
        let result = evaluate("anything", &schema, &config).await;
        // All scores default to 1.0 or assumed valid
        assert!(result.scores.overall > 0.5);
    }

    // ─── Safety scoring ──────────────────────────────────────

    #[test]
    fn test_safety_score_safe_select() {
        let score = compute_safety_score("SELECT id FROM users");
        assert!((score - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_safety_score_drop_table() {
        let score = compute_safety_score("DROP TABLE users");
        assert!(score < 0.6);
    }

    #[test]
    fn test_safety_score_delete_no_where() {
        let score = compute_safety_score("DELETE FROM users");
        assert!(score < 0.8);
    }

    #[test]
    fn test_safety_score_delete_with_where() {
        let score = compute_safety_score("DELETE FROM users WHERE id = 1");
        assert!((score - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_safety_score_update_no_where() {
        let score = compute_safety_score("UPDATE users SET name = 'foo'");
        assert!(score < 0.8);
    }

    #[test]
    fn test_safety_score_update_with_where() {
        let score = compute_safety_score("UPDATE users SET name = 'foo' WHERE id = 1");
        assert!((score - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_safety_score_truncate() {
        let score = compute_safety_score("TRUNCATE TABLE users");
        assert!(score < 0.7);
    }

    #[test]
    fn test_safety_score_grant() {
        let score = compute_safety_score("GRANT ALL ON users TO public");
        assert!(score < 0.8);
    }

    #[test]
    fn test_safety_score_never_negative() {
        let score =
            compute_safety_score("DROP TABLE x; TRUNCATE y; DELETE FROM z; GRANT ALL TO public");
        assert!(score >= 0.0);
    }

    // ─── Complexity scoring ──────────────────────────────────

    #[test]
    fn test_complexity_heuristic_simple() {
        let score = compute_complexity_heuristic("SELECT id FROM users");
        assert!((score - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_complexity_heuristic_join() {
        let score = compute_complexity_heuristic(
            "SELECT u.id FROM users u JOIN orders o ON u.id = o.user_id",
        );
        assert!(score < 1.0);
    }

    #[test]
    fn test_complexity_heuristic_subquery() {
        let score = compute_complexity_heuristic(
            "SELECT id FROM users WHERE id IN (SELECT user_id FROM orders)",
        );
        assert!(score < 1.0);
    }

    #[test]
    fn test_complexity_from_signals_simple() {
        let signals = crate::explainer::types::CostSignals {
            complexity: crate::explainer::types::QueryComplexity::Low,
            complexity_score: 0,
            join_count: 0,
            has_aggregation: false,
            has_subquery: false,
            has_window_function: false,
            has_limit: false,
            has_cross_join: false,
            uses_select_star: false,
            has_distinct: false,
            has_union: false,
            table_count: 1,
            filter_count: 0,
            unfiltered_scans: vec![],
            risk_flags: vec![],
            cost_estimate: None,
            complexity_score_v2: None,
        };
        let score = compute_complexity_from_signals(&signals);
        assert!((score - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_complexity_from_signals_extreme() {
        let signals = crate::explainer::types::CostSignals {
            complexity: crate::explainer::types::QueryComplexity::Extreme,
            complexity_score: 15,
            join_count: 5,
            has_aggregation: true,
            has_subquery: true,
            has_window_function: true,
            has_limit: false,
            has_cross_join: true,
            uses_select_star: true,
            has_distinct: true,
            has_union: true,
            table_count: 6,
            filter_count: 0,
            unfiltered_scans: vec!["t1".to_string()],
            risk_flags: vec![],
            cost_estimate: None,
            complexity_score_v2: None,
        };
        let score = compute_complexity_from_signals(&signals);
        assert!(score < 0.1);
    }

    // ─── Lint score ──────────────────────────────────────────

    #[test]
    fn test_lint_score_clean() {
        let result = crate::linter::types::LintResult {
            sql: "SELECT id FROM users".to_string(),
            findings: vec![],
            error_count: 0,
            warning_count: 0,
            clean: true,
        };
        assert!((compute_lint_score(&result) - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn test_lint_score_with_warning() {
        use crate::linter::types::*;
        let result = LintResult {
            sql: "test".to_string(),
            findings: vec![LintFinding {
                code: "L001".to_string(),
                rule: "test".to_string(),
                severity: LintSeverity::Warning,
                message: "test".to_string(),
                suggestion: None,
                line: None,
                column: None,
            }],
            error_count: 0,
            warning_count: 1,
            clean: false,
        };
        assert!((compute_lint_score(&result) - 0.9).abs() < f64::EPSILON);
    }

    // ─── Observability events ────────────────────────────────

    #[tokio::test]
    async fn test_create_event_basic() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECT id FROM users", &schema).await;
        let event = create_event("SELECT id FROM users", &result);
        assert_eq!(event.event_type, "sql_evaluation");
        assert!(!event.timestamp.is_empty());
        assert_eq!(event.sql, "SELECT id FROM users");
        assert_eq!(event.grade, Grade::A);
    }

    #[tokio::test]
    async fn test_create_event_has_metadata() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECT id FROM users", &schema).await;
        let event = create_event("SELECT id FROM users", &result);
        assert!(event.metadata.contains_key("syntax_valid"));
        assert!(event.metadata.contains_key("grade"));
    }

    #[tokio::test]
    async fn test_create_event_invalid_sql() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECTZ broken", &schema).await;
        let event = create_event("SELECTZ broken", &result);
        assert!(event.error_count > 0 || result.scores.syntax < 1.0);
    }

    #[tokio::test]
    async fn test_create_event_serializes_to_json() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECT id FROM users", &schema).await;
        let event = create_event("SELECT id FROM users", &result);
        let json = serde_json::to_string(&event);
        assert!(json.is_ok());
    }

    // ─── Serialization ───────────────────────────────────────

    #[tokio::test]
    async fn test_eval_result_serializes() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECT id FROM users", &schema).await;
        let json = serde_json::to_string(&result).unwrap();
        assert!(json.contains("overall_grade"));
        assert!(json.contains("scores"));
    }

    #[test]
    fn test_eval_config_serializes() {
        let config = EvalConfig::default();
        let json = serde_json::to_string(&config).unwrap();
        assert!(json.contains("enable_validation"));
    }

    #[tokio::test]
    async fn test_eval_result_time_tracking() {
        let schema = ecommerce_schema();
        let result = evaluate_default("SELECT id FROM users", &schema).await;
        // Should complete quickly
        assert!(result.total_time_ms < 5000);
    }
}
