//! Multi-dimensional complexity scoring engine (0-100).
//!
//! Combines structural analysis, semantic anti-pattern detection,
//! and cost estimation into a single complexity score.

use crate::complexity::types::*;
use crate::explainer::types::{CostSignals, PlanOperation, PlanStep};
use crate::linter::types::LintFinding;

/// Compute the v2 complexity score (0-100) from plan steps, lint findings,
/// and an optional cost estimate.
///
/// # Dimensions
///
/// - **Structural (0-40):** joins, subquery nesting, UNIONs, CTEs
/// - **Semantic (0-30):** anti-pattern findings weighted by severity
/// - **Cost (0-30):** from cost estimate tier or heuristic fallback
pub fn compute_complexity_v2(
    steps: &[PlanStep],
    cost_signals: &CostSignals,
    lint_findings: &[LintFinding],
    cost_estimate: Option<&CostEstimate>,
) -> ComplexityScoreV2 {
    let structural = compute_structural(steps, cost_signals);
    let semantic = compute_semantic(lint_findings);
    let cost = compute_cost_dimension(cost_estimate, cost_signals);

    let total = (structural.score + semantic.score + cost.score).min(100);
    let tier = ComplexityTier::from_score(total);

    let mut factors = Vec::new();
    for f in &structural.factors {
        factors.push(ComplexityFactor {
            name: f.clone(),
            points: 0,
            description: f.clone(),
        });
    }

    ComplexityScoreV2 {
        total,
        structural,
        semantic,
        cost,
        tier,
        factors,
    }
}

/// Compute structural dimension score (0-40).
fn compute_structural(steps: &[PlanStep], signals: &CostSignals) -> DimensionScore {
    let mut score: u32 = 0;
    let mut factors = Vec::new();

    // Joins: 3 points each, max 15
    let join_points = (signals.join_count as u32 * 3).min(15);
    if join_points > 0 {
        score += join_points;
        factors.push(format!(
            "{} join(s): {} pts",
            signals.join_count, join_points
        ));
    }

    // Subquery nesting: exponential (2/4/8/16), max 16
    let nesting_depth = compute_nesting_depth(steps);
    let nesting_points = match nesting_depth {
        0 => 0,
        1 => 2,
        2 => 4,
        3 => 8,
        _ => 16,
    };
    if nesting_points > 0 {
        score += nesting_points;
        factors.push(format!(
            "subquery nesting depth {nesting_depth}: {nesting_points} pts",
        ));
    }

    // UNION branches: 2 points each, max 6
    let union_count = all_steps(steps)
        .iter()
        .filter(|s| matches!(&s.operation, PlanOperation::Union { .. }))
        .count();
    let union_points = (union_count as u32 * 2).min(6);
    if union_points > 0 {
        score += union_points;
        factors.push(format!("{union_count} UNION(s): {union_points} pts"));
    }

    // CTEs (subqueries): 1 point each, max 3
    let cte_count = all_steps(steps)
        .iter()
        .filter(|s| matches!(&s.operation, PlanOperation::Subquery { .. }))
        .count();
    let cte_points = (cte_count as u32).min(3);
    if cte_points > 0 {
        score += cte_points;
        factors.push(format!("{cte_count} CTE/subquery: {cte_points} pts"));
    }

    DimensionScore {
        score: score.min(40),
        max: 40,
        factors,
    }
}

/// Compute semantic dimension score (0-30).
fn compute_semantic(findings: &[LintFinding]) -> DimensionScore {
    let mut score: u32 = 0;
    let mut factors = Vec::new();

    for finding in findings {
        let points = match finding.code.as_str() {
            "L013" => 5,                   // Correlated subquery
            "L015" => 4,                   // NOT IN nullable
            "L023" => 4,                   // Missing partition filter
            "L018" => 3,                   // Implicit cross join
            "L014" => 3,                   // Non-sargable
            "L022" => 3,                   // Large IN list
            "L024" => 3,                   // Non-equi join
            "L017" => 2,                   // UNION without ALL
            "L019" => 2,                   // SELECT * in subquery
            "L020" => 2,                   // OR in JOIN ON
            "L021" => 2,                   // ORDER BY in subquery without LIMIT
            "L025" | "L026" | "L016" => 1, // Info-level
            _ => {
                // Existing rules (L001-L012): use severity-based scoring
                match finding.severity {
                    crate::linter::types::LintSeverity::Error => 3,
                    crate::linter::types::LintSeverity::Warning => 2,
                    crate::linter::types::LintSeverity::Info => 1,
                }
            }
        };
        score += points;
        factors.push(format!("{}: {} pts", finding.code, points));
    }

    DimensionScore {
        score: score.min(30),
        max: 30,
        factors,
    }
}

/// Compute cost dimension score (0-30).
///
/// Uses a more granular scoring model based on bytes scanned ranges:
/// - <1 MB: 0 pts (trivial)
/// - 1-100 MB: 2 pts (small)
/// - 100 MB - 1 GB: 5 pts (moderate)
/// - 1-10 GB: 10 pts (significant)
/// - 10-100 GB: 15 pts (large)
/// - 100 GB - 1 TB: 22 pts (expensive)
/// - >1 TB: 30 pts (dangerous)
///
/// Adds bonus points for warnings (missing partition filter, cross joins).
fn compute_cost_dimension(
    cost_estimate: Option<&CostEstimate>,
    signals: &CostSignals,
) -> DimensionScore {
    let mut factors = Vec::new();

    let score = if let Some(estimate) = cost_estimate {
        if estimate.cost_tier == CostTier::Unknown {
            compute_cost_heuristic(signals, &mut factors)
        } else {
            let mut s = 0u32;

            // Granular bytes-scanned scoring
            let bytes = estimate.bytes_scanned;
            let bytes_score = match bytes {
                0..=999_999 => 0,                        // < 1 MB
                1_000_000..=99_999_999 => 2,             // 1 - 100 MB
                100_000_000..=999_999_999 => 5,          // 100 MB - 1 GB
                1_000_000_000..=9_999_999_999 => 10,     // 1 - 10 GB
                10_000_000_000..=99_999_999_999 => 15,   // 10 - 100 GB
                100_000_000_000..=999_999_999_999 => 22, // 100 GB - 1 TB
                _ => 30,                                 // > 1 TB
            };
            s += bytes_score;
            if bytes_score > 0 {
                let label = match bytes_score {
                    2 => "small scan (1-100 MB)",
                    5 => "moderate scan (100 MB - 1 GB)",
                    10 => "significant scan (1-10 GB)",
                    15 => "large scan (10-100 GB)",
                    22 => "expensive scan (100 GB - 1 TB)",
                    30 => "DANGEROUS scan (>1 TB)",
                    _ => "scan",
                };
                factors.push(format!("{label}: {bytes_score} pts"));
            } else {
                factors.push(format!("{} cost tier: 0 pts", estimate.cost_tier));
            }

            // Bonus for warnings (capped to not exceed 30 total)
            let warning_pts: u32 = estimate
                .warnings
                .iter()
                .map(|w| {
                    if w.contains("CROSS JOIN") {
                        3
                    } else if w.contains("no partition filter") {
                        2
                    } else {
                        0
                    }
                })
                .sum();
            if warning_pts > 0 {
                s += warning_pts;
                factors.push(format!("cost warnings: {warning_pts} pts"));
            }

            s
        }
    } else {
        compute_cost_heuristic(signals, &mut factors)
    };

    DimensionScore {
        score: score.min(30),
        max: 30,
        factors,
    }
}

/// Heuristic cost scoring when no table statistics are available.
fn compute_cost_heuristic(signals: &CostSignals, factors: &mut Vec<String>) -> u32 {
    let mut score = 0u32;

    // Unfiltered scans: 5 points each
    let unfiltered_points = (signals.unfiltered_scans.len() as u32 * 5).min(15);
    if unfiltered_points > 0 {
        score += unfiltered_points;
        factors.push(format!(
            "{} unfiltered scan(s): {} pts",
            signals.unfiltered_scans.len(),
            unfiltered_points
        ));
    }

    // Cross join: 15 points
    if signals.has_cross_join {
        score += 15;
        factors.push("cross join: 15 pts".to_string());
    }

    score
}

/// Compute the maximum nesting depth of subqueries.
fn compute_nesting_depth(steps: &[PlanStep]) -> u32 {
    let mut max_depth = 0u32;
    for step in steps {
        if let PlanOperation::Subquery { inner_steps, .. } = &step.operation {
            let inner_depth = 1 + compute_nesting_depth(inner_steps);
            max_depth = max_depth.max(inner_depth);
        }
    }
    max_depth
}

/// Flatten all steps including nested subqueries.
fn all_steps(steps: &[PlanStep]) -> Vec<&PlanStep> {
    let mut result = Vec::new();
    for step in steps {
        result.push(step);
        if let PlanOperation::Subquery { inner_steps, .. } = &step.operation {
            result.extend(all_steps(inner_steps));
        }
    }
    result
}

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

    fn empty_signals() -> CostSignals {
        CostSignals {
            complexity: 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,
        }
    }

    #[test]
    fn test_simple_query_low_score() {
        let steps = vec![PlanStep {
            step: 1,
            operation: PlanOperation::TableScan {
                table: "users".to_string(),
                columns_read: vec!["id".to_string()],
                filters: vec!["id = 1".to_string()],
            },
            notes: None,
        }];
        let signals = empty_signals();
        let result = compute_complexity_v2(&steps, &signals, &[], None);
        assert!(result.total <= 20);
        assert_eq!(result.tier, ComplexityTier::Simple);
    }

    #[test]
    fn test_join_query_moderate() {
        let steps = vec![
            PlanStep {
                step: 1,
                operation: PlanOperation::TableScan {
                    table: "users".to_string(),
                    columns_read: vec![],
                    filters: vec![],
                },
                notes: None,
            },
            PlanStep {
                step: 2,
                operation: PlanOperation::Join {
                    join_type: JoinKind::Inner,
                    left: "users".to_string(),
                    right: "orders".to_string(),
                    condition: "users.id = orders.user_id".to_string(),
                },
                notes: None,
            },
        ];
        let mut signals = empty_signals();
        signals.join_count = 1;
        let result = compute_complexity_v2(&steps, &signals, &[], None);
        assert!(result.total >= 3); // At least 3 for a join
    }

    #[test]
    fn test_cross_join_high_cost() {
        let steps = vec![];
        let mut signals = empty_signals();
        signals.has_cross_join = true;
        signals.unfiltered_scans = vec!["a".to_string(), "b".to_string()];
        let result = compute_complexity_v2(&steps, &signals, &[], None);
        assert!(result.cost.score >= 15); // Cross join = 15 pts
    }

    #[test]
    fn test_nesting_depth_calculation() {
        let steps = vec![PlanStep {
            step: 1,
            operation: PlanOperation::Subquery {
                alias: "sub1".to_string(),
                inner_steps: vec![PlanStep {
                    step: 1,
                    operation: PlanOperation::Subquery {
                        alias: "sub2".to_string(),
                        inner_steps: vec![],
                    },
                    notes: None,
                }],
            },
            notes: None,
        }];
        assert_eq!(compute_nesting_depth(&steps), 2);
    }

    #[test]
    fn test_dangerous_cost_tier_max_score() {
        let steps = vec![];
        let signals = empty_signals();
        let cost = CostEstimate {
            bytes_scanned: 1_000_000_000_000,
            cost_usd: 500.0,
            cost_tier: CostTier::Dangerous,
            table_breakdown: vec![],
            warnings: vec![],
            disclaimer: None,
            cost_range: None,
            recommendations: vec![],
            confidence: EstimationConfidence::High,
            confidence_factors: vec![],
            estimation_method: None,
            time_based_cost: None,
        };
        let result = compute_complexity_v2(&steps, &signals, &[], Some(&cost));
        assert_eq!(result.cost.score, 30);
    }

    #[test]
    fn test_score_capped_at_100() {
        let steps = vec![];
        let mut signals = empty_signals();
        signals.join_count = 20; // Would be 60 uncapped
        signals.has_cross_join = true;
        signals.unfiltered_scans = vec!["a".to_string(), "b".to_string(), "c".to_string()];
        let result = compute_complexity_v2(&steps, &signals, &[], None);
        assert!(result.total <= 100);
    }

    #[test]
    fn test_backwards_compatible_with_v1() {
        // The old score (0-15) still exists on CostSignals — v2 is additive
        let signals = CostSignals {
            complexity: QueryComplexity::High,
            complexity_score: 8,
            ..empty_signals()
        };
        let steps = vec![];
        let result = compute_complexity_v2(&steps, &signals, &[], None);
        // v2 score should be a valid 0-100 value
        assert!(result.total <= 100);
    }
}
