//! dbt project-wide complexity reporting.
//!
//! Scores all models in a dbt project and generates a sorted report
//! with recommendations for the most complex models.

use std::collections::HashMap;

use crate::complexity::cost_estimator::estimate_cost;
use crate::complexity::scoring::compute_complexity_v2;
use crate::complexity::types::*;
use crate::dbt::types::DbtProject;
use crate::explainer::engine::explain;
use crate::linter::engine::lint;
use crate::schema::types::SchemaDefinition;

/// Generate a complexity report for an entire dbt project.
///
/// Scores each model and sorts by complexity descending. Returns
/// the top-N most complex models with recommendations.
pub async fn generate_dbt_complexity_report(
    project: &DbtProject,
    schema: &SchemaDefinition,
    top_n: usize,
) -> DbtComplexityReport {
    // Build downstream dependency count map
    let downstream_counts = compute_downstream_counts(project);

    let mut model_results = Vec::new();

    for model in &project.models {
        let explanation = explain(&model.sql, schema).await;
        let lint_result = lint(&model.sql, schema);

        let (steps, cost_signals) = if let Some(ref expl) = explanation.explanation {
            (expl.plan_steps.clone(), expl.cost_signals.clone())
        } else {
            (
                vec![],
                crate::explainer::cost::analyze_cost(
                    &[],
                    &crate::explainer::types::QueryLineage {
                        tables: vec![],
                        columns_read: vec![],
                        columns_output: vec![],
                        join_graph: vec![],
                    },
                ),
            )
        };

        let cost_est = estimate_cost(&steps, schema, schema.dialect);
        let complexity = compute_complexity_v2(
            &steps,
            &cost_signals,
            &lint_result.findings,
            Some(&cost_est),
        );

        let downstream_count = downstream_counts.get(&model.name).copied().unwrap_or(0);

        model_results.push(DbtModelComplexity {
            name: model.name.clone(),
            path: model.path.clone(),
            score: complexity.total,
            tier: complexity.tier,
            downstream_count,
            lint_findings: lint_result.findings.len(),
            cost_estimate: if cost_est.cost_tier != CostTier::Unknown {
                Some(cost_est)
            } else {
                None
            },
            complexity,
        });
    }

    // Sort by score descending
    model_results.sort_by(|a, b| b.score.cmp(&a.score));

    // Compute summary
    let total_models = model_results.len();
    let avg_score = if total_models > 0 {
        model_results.iter().map(|m| m.score as f64).sum::<f64>() / total_models as f64
    } else {
        0.0
    };

    let mut tier_counts: HashMap<String, usize> = HashMap::new();
    for m in &model_results {
        *tier_counts.entry(m.tier.to_string()).or_insert(0) += 1;
    }
    let mut tier_distribution: Vec<(String, usize)> = tier_counts.into_iter().collect();
    tier_distribution.sort_by(|a, b| b.1.cmp(&a.1));

    let total_estimated_cost: Option<f64> = {
        let costs: Vec<f64> = model_results
            .iter()
            .filter_map(|m| m.cost_estimate.as_ref())
            .map(|c| c.cost_usd)
            .collect();
        if costs.is_empty() {
            None
        } else {
            Some(costs.iter().sum())
        }
    };

    // Generate recommendations for top-N complex models
    let recommendations = generate_recommendations(&model_results, top_n);

    // Truncate to top_n for the report
    let top_models = if top_n > 0 && model_results.len() > top_n {
        model_results[..top_n].to_vec()
    } else {
        model_results
    };

    DbtComplexityReport {
        project_name: project.name.clone(),
        models: top_models,
        summary: DbtComplexitySummary {
            total_models,
            avg_score,
            tier_distribution,
            total_estimated_cost,
        },
        recommendations,
    }
}

/// Compute how many downstream models depend on each model.
fn compute_downstream_counts(project: &DbtProject) -> HashMap<String, usize> {
    let mut counts: HashMap<String, usize> = HashMap::new();
    for model in &project.models {
        for dep in &model.refs {
            *counts.entry(dep.clone()).or_insert(0) += 1;
        }
    }
    counts
}

/// Generate recommendations for the most complex models.
fn generate_recommendations(models: &[DbtModelComplexity], top_n: usize) -> Vec<String> {
    let mut recs = Vec::new();

    for (i, model) in models.iter().take(top_n).enumerate() {
        if model.score > 80 {
            recs.push(format!(
                "#{}: Model '{}' (score: {}, tier: {}) — consider breaking into smaller models",
                i + 1,
                model.name,
                model.score,
                model.tier
            ));
        } else if model.score > 60 {
            recs.push(format!(
                "#{}: Model '{}' (score: {}, tier: {}) — review for optimization opportunities",
                i + 1,
                model.name,
                model.score,
                model.tier
            ));
        }

        if model.downstream_count > 5 {
            recs.push(format!(
                "  Model '{}' has {} downstream dependents — high impact if slow",
                model.name, model.downstream_count
            ));
        }

        if model.lint_findings > 3 {
            recs.push(format!(
                "  Model '{}' has {} lint findings — address anti-patterns",
                model.name, model.lint_findings
            ));
        }
    }

    recs
}

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

    fn empty_project() -> DbtProject {
        DbtProject {
            name: "test_project".to_string(),
            models: vec![],
            build_order: vec![],
            warnings: vec![],
        }
    }

    fn simple_schema() -> SchemaDefinition {
        use crate::schema::types::*;
        let mut tables = std::collections::HashMap::new();
        tables.insert(
            "users".to_string(),
            TableDef {
                description: None,
                database: None,
                schema: None,
                columns: vec![ColumnDef {
                    name: "id".to_string(),
                    data_type: "INT".to_string(),
                    nullable: false,
                    description: None,
                    tags: vec![],
                    synonyms: vec![],

                    statistics: None,
                }],
                primary_key: None,
                foreign_keys: vec![],
                statistics: None,
                clustering_keys: vec![],
            },
        );
        SchemaDefinition {
            version: "1".to_string(),
            dialect: SqlDialect::Generic,
            tables,
            database: None,
            schema_name: None,
            glossary: None,
        }
    }

    #[tokio::test]
    async fn test_empty_project_report() {
        let project = empty_project();
        let schema = simple_schema();
        let report = generate_dbt_complexity_report(&project, &schema, 10).await;
        assert_eq!(report.project_name, "test_project");
        assert!(report.models.is_empty());
        assert_eq!(report.summary.total_models, 0);
    }

    #[tokio::test]
    async fn test_single_model_report() {
        let project = DbtProject {
            name: "test_project".to_string(),
            models: vec![DbtModel {
                name: "users_active".to_string(),
                path: "models/users_active.sql".to_string(),
                sql: "SELECT id FROM users WHERE id > 0".to_string(),
                raw_sql: "SELECT id FROM users WHERE id > 0".to_string(),
                materialization: Some("table".to_string()),
                description: None,
                refs: vec![],
                sources: vec![],
            }],
            build_order: vec!["users_active".to_string()],
            warnings: vec![],
        };
        let schema = simple_schema();
        let report = generate_dbt_complexity_report(&project, &schema, 10).await;
        assert_eq!(report.summary.total_models, 1);
        assert_eq!(report.models.len(), 1);
        assert_eq!(report.models[0].name, "users_active");
    }

    #[test]
    fn test_downstream_counts() {
        let project = DbtProject {
            name: "test".to_string(),
            models: vec![
                DbtModel {
                    name: "a".to_string(),
                    path: "a.sql".to_string(),
                    sql: "SELECT 1".to_string(),
                    raw_sql: "SELECT 1".to_string(),
                    materialization: None,
                    description: None,
                    refs: vec!["base".to_string()],
                    sources: vec![],
                },
                DbtModel {
                    name: "b".to_string(),
                    path: "b.sql".to_string(),
                    sql: "SELECT 1".to_string(),
                    raw_sql: "SELECT 1".to_string(),
                    materialization: None,
                    description: None,
                    refs: vec!["base".to_string()],
                    sources: vec![],
                },
            ],
            build_order: vec![],
            warnings: vec![],
        };
        let counts = compute_downstream_counts(&project);
        assert_eq!(counts.get("base"), Some(&2));
    }

    #[test]
    fn test_recommendations_generated_for_complex() {
        let models = vec![DbtModelComplexity {
            name: "complex_model".to_string(),
            path: "models/complex_model.sql".to_string(),
            score: 85,
            tier: ComplexityTier::Extreme,
            downstream_count: 8,
            lint_findings: 5,
            cost_estimate: None,
            complexity: ComplexityScoreV2 {
                total: 85,
                structural: DimensionScore {
                    score: 35,
                    max: 40,
                    factors: vec![],
                },
                semantic: DimensionScore {
                    score: 25,
                    max: 30,
                    factors: vec![],
                },
                cost: DimensionScore {
                    score: 25,
                    max: 30,
                    factors: vec![],
                },
                tier: ComplexityTier::Extreme,
                factors: vec![],
            },
        }];
        let recs = generate_recommendations(&models, 5);
        assert!(!recs.is_empty());
        assert!(recs.iter().any(|r| r.contains("breaking into smaller")));
        assert!(recs.iter().any(|r| r.contains("downstream")));
        assert!(recs.iter().any(|r| r.contains("lint findings")));
    }
}
