//! SQL linting engine.
//!
//! Runs all lint rules against a SQL query and collects findings.
//! Also integrates polyglot-sql tagger rules (L027-L031) for multi-dialect
//! anti-pattern detection.

use super::rules::all_rules;
use super::types::*;
use crate::schema::types::SchemaDefinition;
use crate::tagger;

/// Lint a SQL query for anti-patterns and best practice violations.
///
/// Runs sqlparser-based rules (L001-L026) and polyglot-sql tagger rules (L027-L031).
pub fn lint(sql: &str, schema: &SchemaDefinition) -> LintResult {
    let rules = all_rules();
    let mut findings = Vec::new();

    for rule in &rules {
        let rule_findings = rule.check(sql, schema);
        findings.extend(rule_findings);
    }

    // Run polyglot-sql tagger rules and convert to LintFindings
    let tagger_findings = run_tagger_rules(sql, schema);
    findings.extend(tagger_findings);

    // Sort findings by line number (None goes last)
    findings.sort_by(|a, b| {
        let line_a = a.line.unwrap_or(usize::MAX);
        let line_b = b.line.unwrap_or(usize::MAX);
        line_a.cmp(&line_b).then_with(|| a.code.cmp(&b.code))
    });

    let error_count = findings
        .iter()
        .filter(|f| matches!(f.severity, LintSeverity::Error))
        .count();
    let warning_count = findings
        .iter()
        .filter(|f| matches!(f.severity, LintSeverity::Warning))
        .count();
    let clean = findings.is_empty();

    LintResult {
        sql: sql.to_string(),
        findings,
        error_count,
        warning_count,
        clean,
    }
}

/// Run polyglot-sql tagger rules and convert results to LintFindings.
///
/// Maps triggered tags to lint codes:
/// - `filter_has_func` -> L027 (Warning)
/// - `join_has_func` -> L028 (Warning)
/// - `agg_before_join` -> L029 (Error)
/// - `select_without_limit` -> L030 (Warning)
/// - `create_or_replace_table` -> L031 (Info)
///
/// Note: `select_star` is NOT mapped here — L001 already handles it via sqlparser.
fn run_tagger_rules(sql: &str, schema: &SchemaDefinition) -> Vec<LintFinding> {
    let dialect = dialect_to_polyglot(&schema.dialect);

    // Skip select_star since L001 already covers it
    let config = tagger::AnalysisConfig {
        skip_tags: vec!["select_star".to_string()],
    };

    let tag_results = tagger::analyze_tags(sql, dialect, &config);
    let mut findings = Vec::new();

    for result in &tag_results {
        if !result.triggered {
            continue;
        }

        let (code, rule_name, severity, suggestion) = match result.tag_name.as_str() {
            "filter_has_func" => (
                "L027",
                "function-on-filter-column",
                LintSeverity::Warning,
                Some("Move the function to the right side of the comparison or create a computed/functional index".to_string()),
            ),
            "join_has_func" => (
                "L028",
                "function-on-join-column",
                LintSeverity::Warning,
                Some("Remove the function from the JOIN condition or pre-compute the value in a CTE".to_string()),
            ),
            "agg_before_join" => (
                "L029",
                "aggregation-before-join",
                LintSeverity::Error,
                Some("Consider joining first, then aggregating, or verify this is intentional".to_string()),
            ),
            "select_without_limit" => (
                "L030",
                "select-without-limit",
                LintSeverity::Warning,
                Some("Add a LIMIT clause to prevent unbounded result sets".to_string()),
            ),
            "create_or_replace_table" => (
                "L031",
                "create-or-replace-table",
                LintSeverity::Info,
                Some("CREATE OR REPLACE TABLE drops existing data; consider CREATE TABLE IF NOT EXISTS instead".to_string()),
            ),
            _ => continue,
        };

        let message = build_tag_message(&result.tag_name, &result.reports);

        findings.push(LintFinding {
            code: code.to_string(),
            rule: rule_name.to_string(),
            severity,
            message,
            suggestion,
            line: None,
            column: None,
        });
    }

    findings
}

/// Build a human-readable message from tag reports.
fn build_tag_message(tag_name: &str, reports: &[tagger::TagReport]) -> String {
    match tag_name {
        "filter_has_func" => {
            let funcs: Vec<&str> = reports
                .iter()
                .filter_map(|r| r.functions.as_ref())
                .flat_map(|fs| fs.iter().map(|s| s.as_str()))
                .collect();
            if funcs.is_empty() {
                "Function applied to column in WHERE clause prevents index usage".to_string()
            } else {
                format!(
                    "Function applied to column in WHERE clause prevents index usage: {}",
                    funcs.join(", ")
                )
            }
        }
        "join_has_func" => {
            let funcs: Vec<&str> = reports
                .iter()
                .filter_map(|r| r.functions.as_ref())
                .flat_map(|fs| fs.iter().map(|s| s.as_str()))
                .collect();
            if funcs.is_empty() {
                "Function applied to column in JOIN condition prevents join optimization"
                    .to_string()
            } else {
                format!(
                    "Function applied to column in JOIN condition prevents join optimization: {}",
                    funcs.join(", ")
                )
            }
        }
        "agg_before_join" => {
            "Aggregation performed before JOIN — consider joining first, then aggregating"
                .to_string()
        }
        "select_without_limit" => "SELECT without LIMIT may return unbounded results".to_string(),
        "create_or_replace_table" => {
            "CREATE OR REPLACE TABLE silently drops and recreates the table".to_string()
        }
        _ => format!("Tag triggered: {}", tag_name),
    }
}

/// Map altimate-core SqlDialect to polyglot DialectType.
fn dialect_to_polyglot(
    dialect: &crate::schema::types::SqlDialect,
) -> polyglot_sql::dialects::DialectType {
    use crate::schema::types::SqlDialect;
    use polyglot_sql::dialects::DialectType;

    match dialect {
        SqlDialect::Snowflake => DialectType::Snowflake,
        SqlDialect::Postgres => DialectType::PostgreSQL,
        SqlDialect::BigQuery => DialectType::BigQuery,
        SqlDialect::Databricks => DialectType::Databricks,
        SqlDialect::DuckDB => DialectType::DuckDB,
        SqlDialect::MySQL => DialectType::MySQL,
        SqlDialect::Redshift => DialectType::Redshift,
        SqlDialect::Hive => DialectType::Hive,
        SqlDialect::Spark => DialectType::Spark,
        SqlDialect::SQLite => DialectType::SQLite,
        SqlDialect::Trino => DialectType::Trino,
        SqlDialect::Presto => DialectType::Presto,
        SqlDialect::ClickHouse => DialectType::ClickHouse,
        SqlDialect::Oracle => DialectType::Oracle,
        SqlDialect::TSQL => DialectType::TSQL,
        SqlDialect::Athena => DialectType::Athena,
        _ => DialectType::Snowflake, // Default to Snowflake for unsupported dialects
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::schema::types::*;
    use std::collections::HashMap;

    fn empty_schema() -> SchemaDefinition {
        SchemaDefinition {
            version: "1".to_string(),
            dialect: SqlDialect::Generic,
            tables: HashMap::new(),
            database: None,
            schema_name: None,
            glossary: None,
        }
    }

    #[test]
    fn test_lint_clean_query() {
        let result = lint(
            "SELECT id, name FROM users WHERE id = 1 LIMIT 100",
            &empty_schema(),
        );
        assert!(result.clean);
        assert_eq!(result.error_count, 0);
        assert_eq!(result.warning_count, 0);
    }

    #[test]
    fn test_lint_multiple_findings() {
        let result = lint("SELECT * FROM users WHERE name = NULL", &empty_schema());
        assert!(!result.clean);
        // Should find L001 (SELECT *) and L009 (= NULL)
        let codes: Vec<&str> = result.findings.iter().map(|f| f.code.as_str()).collect();
        assert!(codes.contains(&"L001"));
        assert!(codes.contains(&"L009"));
    }

    #[test]
    fn test_lint_delete_no_where() {
        let result = lint("DELETE FROM users", &empty_schema());
        assert!(!result.clean);
        assert!(result.error_count > 0);
        let codes: Vec<&str> = result.findings.iter().map(|f| f.code.as_str()).collect();
        assert!(codes.contains(&"L003"));
    }

    #[test]
    fn test_lint_update_no_where() {
        let result = lint("UPDATE users SET name = 'foo'", &empty_schema());
        assert!(!result.clean);
        let codes: Vec<&str> = result.findings.iter().map(|f| f.code.as_str()).collect();
        assert!(codes.contains(&"L002"));
    }

    #[test]
    fn test_lint_sorted_by_line() {
        let result = lint("SELECT * FROM users WHERE name = NULL", &empty_schema());
        // Verify findings are sorted by line number
        for window in result.findings.windows(2) {
            let line_a = window[0].line.unwrap_or(usize::MAX);
            let line_b = window[1].line.unwrap_or(usize::MAX);
            assert!(line_a <= line_b);
        }
    }
}
