//! Lux Linter - Static analysis for idiomatic, correct Lux code
//!
//! Design principles (informed by Clippy, Ruff, Elm, Go vet):
//! - Near-zero false positives in default mode
//! - Speed is non-negotiable (single-pass AST walk)
//! - Auto-fix where possible
//! - Teach idiomatic Lux patterns
//! - Lux-specific lints (effects, behavioral types, schema)
//!
//! Lint categories:
//! - `correctness` (deny)   — definite bugs
//! - `suspicious`  (warn)   — likely bugs
//! - `idiom`       (warn)   — non-idiomatic patterns
//! - `performance` (warn)   — missed optimizations
//! - `style`       (allow)  — formatting/naming preferences
//! - `pedantic`    (allow)  — aggressive, may have false positives

use crate::ast::*;
use crate::diagnostics::{c, colors};
use std::collections::{HashMap, HashSet};

/// Severity for lint diagnostics (separate from compiler errors)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LintLevel {
    /// Must fix — blocks CI by default
    Deny,
    /// Should fix — shown as warning
    Warn,
    /// Informational — off by default
    Allow,
}

/// Lint category following the Clippy model
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LintCategory {
    Correctness,
    Suspicious,
    Idiom,
    Performance,
    Style,
    Pedantic,
}

impl LintCategory {
    fn default_level(&self) -> LintLevel {
        match self {
            LintCategory::Correctness => LintLevel::Deny,
            LintCategory::Suspicious => LintLevel::Warn,
            LintCategory::Idiom => LintLevel::Warn,
            LintCategory::Performance => LintLevel::Warn,
            LintCategory::Style => LintLevel::Allow,
            LintCategory::Pedantic => LintLevel::Allow,
        }
    }

    pub fn category_name(&self) -> &'static str {
        match self {
            LintCategory::Correctness => "correctness",
            LintCategory::Suspicious => "suspicious",
            LintCategory::Idiom => "idiom",
            LintCategory::Performance => "performance",
            LintCategory::Style => "style",
            LintCategory::Pedantic => "pedantic",
        }
    }
}

/// A specific lint rule
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum LintId {
    // Correctness
    UnreachableCode,
    UnusedMustUse,

    // Suspicious
    UnusedVariable,
    UnusedImport,
    UnusedFunction,
    ShadowingBinding,

    // Idiom
    CouldBePure,
    CouldBeTotal,
    UnnecessaryEffectDecl,
    SingleArmMatch,
    WildcardOnSmallEnum,
    ManualMapOption,
    RedundantClone,

    // Performance
    UnnecessaryAllocation,
    LargeClosureCapture,

    // Style
    NamingConvention,
    MissingDocComment,
    TooManyParameters,
    LongFunction,

    // Pedantic
    ImplicitReturn,
    UnqualifiedEffectOp,
}

impl LintId {
    pub fn category(&self) -> LintCategory {
        match self {
            LintId::UnreachableCode | LintId::UnusedMustUse => LintCategory::Correctness,

            LintId::UnusedVariable
            | LintId::UnusedImport
            | LintId::UnusedFunction
            | LintId::ShadowingBinding => LintCategory::Suspicious,

            LintId::CouldBePure
            | LintId::CouldBeTotal
            | LintId::UnnecessaryEffectDecl
            | LintId::SingleArmMatch
            | LintId::WildcardOnSmallEnum
            | LintId::ManualMapOption
            | LintId::RedundantClone => LintCategory::Idiom,

            LintId::UnnecessaryAllocation | LintId::LargeClosureCapture => {
                LintCategory::Performance
            }

            LintId::NamingConvention
            | LintId::MissingDocComment
            | LintId::TooManyParameters
            | LintId::LongFunction => LintCategory::Style,

            LintId::ImplicitReturn | LintId::UnqualifiedEffectOp => LintCategory::Pedantic,
        }
    }

    pub fn name(&self) -> &'static str {
        match self {
            LintId::UnreachableCode => "unreachable-code",
            LintId::UnusedMustUse => "unused-must-use",
            LintId::UnusedVariable => "unused-variable",
            LintId::UnusedImport => "unused-import",
            LintId::UnusedFunction => "unused-function",
            LintId::ShadowingBinding => "shadowing-binding",
            LintId::CouldBePure => "could-be-pure",
            LintId::CouldBeTotal => "could-be-total",
            LintId::UnnecessaryEffectDecl => "unnecessary-effect-decl",
            LintId::SingleArmMatch => "single-arm-match",
            LintId::WildcardOnSmallEnum => "wildcard-on-small-enum",
            LintId::ManualMapOption => "manual-map-option",
            LintId::RedundantClone => "redundant-clone",
            LintId::UnnecessaryAllocation => "unnecessary-allocation",
            LintId::LargeClosureCapture => "large-closure-capture",
            LintId::NamingConvention => "naming-convention",
            LintId::MissingDocComment => "missing-doc-comment",
            LintId::TooManyParameters => "too-many-parameters",
            LintId::LongFunction => "long-function",
            LintId::ImplicitReturn => "implicit-return",
            LintId::UnqualifiedEffectOp => "unqualified-effect-op",
        }
    }

    pub fn message(&self) -> &'static str {
        match self {
            LintId::UnreachableCode => "code after this expression is unreachable",
            LintId::UnusedMustUse => "result of operation should be used",
            LintId::UnusedVariable => "unused variable",
            LintId::UnusedImport => "unused import",
            LintId::UnusedFunction => "function is never called",
            LintId::ShadowingBinding => "binding shadows an existing variable",
            LintId::CouldBePure => "function performs no effects and could be marked `pure`",
            LintId::CouldBeTotal => "function always terminates and could be marked `total`",
            LintId::UnnecessaryEffectDecl => "function declares an effect it never uses",
            LintId::SingleArmMatch => "match with a single arm could be a `let` binding",
            LintId::WildcardOnSmallEnum => "wildcard pattern on type with few variants",
            LintId::ManualMapOption => "manual Option matching could use Option.map",
            LintId::RedundantClone => "value is cloned but only used once",
            LintId::UnnecessaryAllocation => "allocation could be avoided",
            LintId::LargeClosureCapture => "closure captures many variables from environment",
            LintId::NamingConvention => "name doesn't follow Lux conventions",
            LintId::MissingDocComment => "public function is missing documentation",
            LintId::TooManyParameters => "function has many parameters, consider using a record",
            LintId::LongFunction => "function body is very long, consider splitting",
            LintId::ImplicitReturn => "function returns implicitly from last expression",
            LintId::UnqualifiedEffectOp => "effect operation used without explicit handler",
        }
    }

    pub fn explanation(&self) -> &'static str {
        match self {
            LintId::CouldBePure => concat!(
                "This function doesn't use any effects and always produces the same output\n",
                "for the same inputs. Adding `is pure` lets the compiler optimize calls\n",
                "and documents that this function has no side effects.\n",
                "\n",
                "Example:\n",
                "  fn add(a: Int, b: Int): Int is pure = a + b\n",
            ),
            LintId::CouldBeTotal => concat!(
                "This function always terminates — it has no unbounded recursion or infinite\n",
                "loops. Adding `is total` lets the compiler prove termination and enables\n",
                "optimizations like memoization.\n",
                "\n",
                "Example:\n",
                "  fn factorial(n: Int): Int is total = if n <= 1 then 1 else n * factorial(n - 1)\n",
            ),
            LintId::UnusedVariable => concat!(
                "This variable is declared but never referenced. If intentional, prefix\n",
                "the name with an underscore: `_unused`.\n",
            ),
            LintId::UnusedImport => concat!(
                "This import is not used anywhere in the file. Removing it keeps the\n",
                "dependency graph clean and speeds up compilation.\n",
            ),
            LintId::SingleArmMatch => concat!(
                "A match expression with only one arm is equivalent to a let binding:\n",
                "\n",
                "  // Before:\n",
                "  match value { Some(x) => x + 1 }\n",
                "\n",
                "  // After:\n",
                "  let Some(x) = value\n",
                "  x + 1\n",
            ),
            LintId::UnnecessaryEffectDecl => concat!(
                "This function declares an effect in its signature but never performs\n",
                "any operations from that effect. Remove it to simplify the signature.\n",
                "\n",
                "  // Before:\n",
                "  fn add(a: Int, b: Int): Int with {Console} = a + b\n",
                "\n",
                "  // After:\n",
                "  fn add(a: Int, b: Int): Int = a + b\n",
            ),
            LintId::NamingConvention => concat!(
                "Lux conventions:\n",
                "  - Functions and variables: camelCase (e.g., getUserName)\n",
                "  - Types and effects: PascalCase (e.g., UserProfile)\n",
                "  - Constants: SCREAMING_SNAKE (e.g., MAX_SIZE)\n",
            ),
            LintId::MissingDocComment => concat!(
                "Public functions should have documentation comments explaining\n",
                "their purpose, parameters, and return value. Add a doc comment\n",
                "on the line above the function declaration.\n",
            ),
            LintId::TooManyParameters => concat!(
                "Functions with many parameters are hard to call correctly.\n",
                "Consider grouping related parameters into a record type.\n",
                "\n",
                "  // Before:\n",
                "  fn createUser(name: String, email: String, age: Int, role: String): User\n",
                "\n",
                "  // After:\n",
                "  type CreateUserRequest = { name: String, email: String, age: Int, role: String }\n",
                "  fn createUser(request: CreateUserRequest): User\n",
            ),
            _ => "",
        }
    }
}

/// A single lint diagnostic with source location and optional fix
#[derive(Debug, Clone)]
pub struct Lint {
    pub id: LintId,
    pub level: LintLevel,
    pub span: Span,
    pub message: String,
    pub note: Option<String>,
    pub fix: Option<LintFix>,
}

/// A suggested code fix
#[derive(Debug, Clone)]
pub struct LintFix {
    pub description: String,
    pub replacement: String,
    pub span: Span,
}

/// Linter configuration
#[derive(Debug, Clone)]
pub struct LintConfig {
    /// Override levels for specific categories
    pub category_overrides: HashMap<LintCategory, LintLevel>,
    /// Override levels for specific lint IDs
    pub lint_overrides: HashMap<LintId, LintLevel>,
    /// Maximum function parameters before warning
    pub max_params: usize,
    /// Maximum function body line count before warning
    pub max_function_lines: usize,
}

impl Default for LintConfig {
    fn default() -> Self {
        Self {
            category_overrides: HashMap::new(),
            lint_overrides: HashMap::new(),
            max_params: 5,
            max_function_lines: 50,
        }
    }
}

impl LintConfig {
    /// Get the effective level for a lint
    pub fn level_for(&self, id: LintId) -> LintLevel {
        if let Some(&level) = self.lint_overrides.get(&id) {
            return level;
        }
        if let Some(&level) = self.category_overrides.get(&id.category()) {
            return level;
        }
        id.category().default_level()
    }

    /// Enable all lints in a category at the given level
    pub fn set_category(&mut self, category: LintCategory, level: LintLevel) {
        self.category_overrides.insert(category, level);
    }
}

/// The linter: walks the AST and collects diagnostics
pub struct Linter {
    config: LintConfig,
    lints: Vec<Lint>,
    /// Variables defined in the current scope
    defined_vars: Vec<HashSet<String>>,
    /// Variables referenced (used)
    used_vars: HashSet<String>,
    /// Functions defined at top level
    defined_functions: HashSet<String>,
    /// Functions called
    called_functions: HashSet<String>,
    /// Imported names
    imported_names: HashSet<String>,
    /// Names actually used from imports
    used_imported_names: HashSet<String>,
    /// Names of all imported modules
    imported_modules: HashSet<String>,
    /// Source code for line counting
    source: String,
}

impl Linter {
    pub fn new(config: LintConfig, source: &str) -> Self {
        Self {
            config,
            lints: Vec::new(),
            defined_vars: vec![HashSet::new()],
            used_vars: HashSet::new(),
            defined_functions: HashSet::new(),
            called_functions: HashSet::new(),
            imported_names: HashSet::new(),
            used_imported_names: HashSet::new(),
            imported_modules: HashSet::new(),
            source: source.to_string(),
        }
    }

    /// Run all lint passes on a program and return diagnostics
    pub fn lint(mut self, program: &Program) -> Vec<Lint> {
        // Pass 1: Collect definitions and references
        self.collect_definitions(program);

        // Pass 2: Walk the AST for pattern-based lints
        self.check_program(program);

        // Pass 3: Cross-reference checks (unused vars, imports, functions)
        self.check_unused();

        // Filter by config level and return
        let config = self.config;
        self.lints
            .into_iter()
            .filter(|l| config.level_for(l.id) != LintLevel::Allow)
            .collect()
    }

    // ── Pass 1: Collect definitions ────────────────────────────────────────

    fn collect_definitions(&mut self, program: &Program) {
        // Collect imports
        for import in &program.imports {
            if let Some(items) = &import.items {
                for item in items {
                    self.imported_names.insert(item.name.clone());
                }
            }
            if import.wildcard {
                // Wildcard imports — track module name
                for seg in &import.path.segments {
                    self.imported_modules.insert(seg.name.clone());
                }
            }
            if let Some(alias) = &import.alias {
                self.imported_names.insert(alias.name.clone());
            }
        }

        // Collect top-level definitions
        for decl in &program.declarations {
            match decl {
                Declaration::Function(f) => {
                    self.defined_functions.insert(f.name.name.clone());
                }
                Declaration::Let(l) => {
                    self.define_var(&l.name.name);
                }
                _ => {}
            }
        }

        // Walk all expressions to find variable and function uses
        for decl in &program.declarations {
            match decl {
                Declaration::Function(f) => {
                    for param in &f.params {
                        self.define_var(&param.name.name);
                    }
                    self.collect_refs_expr(&f.body);
                }
                Declaration::Let(l) => {
                    self.collect_refs_expr(&l.value);
                }
                _ => {}
            }
        }
    }

    fn collect_refs_expr(&mut self, expr: &Expr) {
        match expr {
            Expr::Var(ident) => {
                self.use_var(&ident.name);
                self.called_functions.insert(ident.name.clone());
            }
            Expr::Call { func, args, .. } => {
                // Track function name usage
                if let Expr::Var(name) = func.as_ref() {
                    self.called_functions.insert(name.name.clone());
                    self.use_var(&name.name);
                }
                // Track qualified calls (Module.func)
                if let Expr::Field { object, field, .. } = func.as_ref() {
                    if let Expr::Var(module) = object.as_ref() {
                        self.used_imported_names.insert(module.name.clone());
                        self.imported_modules
                            .iter()
                            .find(|m| **m == module.name)
                            .cloned()
                            .map(|m| self.used_imported_names.insert(m));
                    }
                    self.use_var(&field.name);
                }
                self.collect_refs_expr(func);
                for arg in args {
                    self.collect_refs_expr(arg);
                }
            }
            Expr::BinaryOp { left, right, .. } => {
                self.collect_refs_expr(left);
                self.collect_refs_expr(right);
            }
            Expr::UnaryOp { operand, .. } => {
                self.collect_refs_expr(operand);
            }
            Expr::If {
                condition,
                then_branch,
                else_branch,
                ..
            } => {
                self.collect_refs_expr(condition);
                self.collect_refs_expr(then_branch);
                self.collect_refs_expr(else_branch);
            }
            Expr::Let {
                name, value, body, ..
            } => {
                self.define_var(&name.name);
                self.collect_refs_expr(value);
                self.collect_refs_expr(body);
            }
            Expr::Block {
                statements, result, ..
            } => {
                for stmt in statements {
                    match stmt {
                        Statement::Expr(e) => self.collect_refs_expr(e),
                        Statement::Let { value, name, .. } => {
                            self.define_var(&name.name);
                            self.collect_refs_expr(value);
                        }
                    }
                }
                self.collect_refs_expr(result);
            }
            Expr::Lambda { body, params, .. } => {
                for p in params {
                    self.define_var(&p.name.name);
                }
                self.collect_refs_expr(body);
            }
            Expr::Match { scrutinee, arms, .. } => {
                self.collect_refs_expr(scrutinee);
                for arm in arms {
                    self.collect_refs_pattern(&arm.pattern);
                    if let Some(guard) = &arm.guard {
                        self.collect_refs_expr(guard);
                    }
                    self.collect_refs_expr(&arm.body);
                }
            }
            Expr::Field { object, .. } => {
                self.collect_refs_expr(object);
            }
            Expr::Record { fields, .. } => {
                for (_, val) in fields {
                    self.collect_refs_expr(val);
                }
            }
            Expr::Tuple { elements, .. } | Expr::List { elements, .. } => {
                for e in elements {
                    self.collect_refs_expr(e);
                }
            }
            Expr::EffectOp { effect, args, .. } => {
                self.use_var(&effect.name);
                self.used_imported_names.insert(effect.name.clone());
                for arg in args {
                    self.collect_refs_expr(arg);
                }
            }
            Expr::Run { expr, handlers, .. } => {
                self.collect_refs_expr(expr);
                for (_name, handler_expr) in handlers {
                    self.collect_refs_expr(handler_expr);
                }
            }
            Expr::Resume { value, .. } => {
                self.collect_refs_expr(value);
            }
            Expr::Literal(_) => {}
        }
    }

    fn collect_refs_pattern(&mut self, pattern: &Pattern) {
        match pattern {
            Pattern::Var(ident) => {
                self.define_var(&ident.name);
            }
            Pattern::Constructor { fields, .. } => {
                for f in fields {
                    self.collect_refs_pattern(f);
                }
            }
            Pattern::Tuple { elements, .. } => {
                for e in elements {
                    self.collect_refs_pattern(e);
                }
            }
            Pattern::Record { fields, .. } => {
                for (_, p) in fields {
                    self.collect_refs_pattern(p);
                }
            }
            Pattern::Wildcard(_) | Pattern::Literal(_) => {}
        }
    }

    // ── Pass 2: Pattern-based lints ────────────────────────────────────────

    fn check_program(&mut self, program: &Program) {
        for decl in &program.declarations {
            self.check_declaration(decl);
        }
    }

    fn check_declaration(&mut self, decl: &Declaration) {
        match decl {
            Declaration::Function(f) => self.check_function(f),
            Declaration::Let(l) => self.check_expr(&l.value),
            _ => {}
        }
    }

    fn check_function(&mut self, f: &FunctionDecl) {
        // Style: naming convention (functions should be camelCase)
        if self.config.level_for(LintId::NamingConvention) != LintLevel::Allow {
            self.check_function_naming(f);
        }

        // Style: too many parameters
        if f.params.len() > self.config.max_params {
            self.emit(
                LintId::TooManyParameters,
                f.span,
                format!(
                    "function '{}' has {} parameters (max: {})",
                    f.name.name,
                    f.params.len(),
                    self.config.max_params
                ),
            );
        }

        // Style: missing doc comment on public function
        if f.visibility == Visibility::Public && f.doc.is_none() {
            self.emit(
                LintId::MissingDocComment,
                f.name.span,
                format!(
                    "public function '{}' is missing a doc comment",
                    f.name.name
                ),
            );
        }

        // Style: long function
        if self.config.level_for(LintId::LongFunction) != LintLevel::Allow {
            let body_lines = self.count_span_lines(f.body.span());
            if body_lines > self.config.max_function_lines {
                self.emit(
                    LintId::LongFunction,
                    f.span,
                    format!(
                        "function '{}' is {} lines long (max: {})",
                        f.name.name, body_lines, self.config.max_function_lines
                    ),
                );
            }
        }

        // Idiom: could-be-pure (no effects used and no effect operations in body)
        if !f.properties.contains(&BehavioralProperty::Pure)
            && f.effects.is_empty()
            && !self.expr_has_effects(&f.body)
            && !f.name.name.starts_with("test_")
            && !f.name.name.starts_with("main")
        {
            self.emit(
                LintId::CouldBePure,
                f.name.span,
                format!(
                    "function '{}' performs no effects and could be marked `is pure`",
                    f.name.name
                ),
            );
        }

        // Idiom: unnecessary effect declaration
        if !f.effects.is_empty() && !self.expr_has_effects(&f.body) {
            for effect in &f.effects {
                self.emit(
                    LintId::UnnecessaryEffectDecl,
                    effect.span,
                    format!(
                        "function '{}' declares effect '{}' but never uses it",
                        f.name.name, effect.name
                    ),
                );
            }
        }

        // Check the body
        self.check_expr(&f.body);
    }

    fn check_expr(&mut self, expr: &Expr) {
        match expr {
            Expr::Match { arms, span, .. } => {
                // Idiom: single-arm match
                if arms.len() == 1 && arms[0].guard.is_none() {
                    self.emit(
                        LintId::SingleArmMatch,
                        *span,
                        "match with a single arm could be a `let` binding".to_string(),
                    );
                }

                // Recurse
                for arm in arms {
                    self.check_expr(&arm.body);
                    if let Some(guard) = &arm.guard {
                        self.check_expr(guard);
                    }
                }
            }
            Expr::Block {
                statements, result, ..
            } => {
                for stmt in statements {
                    match stmt {
                        Statement::Expr(e) => self.check_expr(e),
                        Statement::Let { value, .. } => self.check_expr(value),
                    }
                }
                self.check_expr(result);
            }
            Expr::If {
                condition,
                then_branch,
                else_branch,
                ..
            } => {
                self.check_expr(condition);
                self.check_expr(then_branch);
                self.check_expr(else_branch);
            }
            Expr::Let { value, body, .. } => {
                self.check_expr(value);
                self.check_expr(body);
            }
            Expr::Lambda { body, .. } => {
                self.check_expr(body);
            }
            Expr::Call { func, args, .. } => {
                self.check_expr(func);
                for arg in args {
                    self.check_expr(arg);
                }
            }
            Expr::BinaryOp { left, right, .. } => {
                self.check_expr(left);
                self.check_expr(right);
            }
            Expr::UnaryOp { operand, .. } => {
                self.check_expr(operand);
            }
            Expr::Run { expr, .. } => {
                self.check_expr(expr);
            }
            _ => {}
        }
    }

    // ── Pass 3: Cross-reference checks ─────────────────────────────────────

    fn check_unused(&mut self) {
        let mut pending_lints = Vec::new();

        // Check for unused variables (excluding underscore-prefixed)
        for scope in &self.defined_vars {
            for var in scope {
                if !var.starts_with('_')
                    && !self.used_vars.contains(var)
                    && !self.defined_functions.contains(var)
                {
                    pending_lints.push((
                        LintId::UnusedVariable,
                        Span { start: 0, end: 0 },
                        format!("variable '{}' is never used (prefix with _ to silence)", var),
                    ));
                }
            }
        }

        // Check for unused imports
        for name in &self.imported_names {
            if !self.used_vars.contains(name)
                && !self.used_imported_names.contains(name)
                && !self.called_functions.contains(name)
            {
                pending_lints.push((
                    LintId::UnusedImport,
                    Span { start: 0, end: 0 },
                    format!("import '{}' is never used", name),
                ));
            }
        }

        // Check for unused top-level private functions
        for func_name in &self.defined_functions {
            if !self.called_functions.contains(func_name)
                && func_name != "main"
                && !func_name.starts_with("test_")
                && !func_name.starts_with('_')
            {
                pending_lints.push((
                    LintId::UnusedFunction,
                    Span { start: 0, end: 0 },
                    format!("function '{}' is defined but never called", func_name),
                ));
            }
        }

        // Emit all collected lints
        for (id, span, message) in pending_lints {
            self.emit(id, span, message);
        }
    }

    // ── Helpers ────────────────────────────────────────────────────────────

    /// Check if an expression contains effect operations
    fn expr_has_effects(&self, expr: &Expr) -> bool {
        match expr {
            Expr::EffectOp { .. } => true,
            Expr::Call { func, args, .. } => {
                // Check if calling a qualified effect (Console.print, etc.)
                if let Expr::Field { object, .. } = func.as_ref() {
                    if let Expr::Var(module) = object.as_ref() {
                        let effect_names = [
                            "Console", "File", "Http", "HttpServer", "Random", "Time",
                            "Process", "State", "Fail", "Test", "Concurrent", "Channel",
                        ];
                        if effect_names.contains(&module.name.as_str()) {
                            return true;
                        }
                    }
                }
                self.expr_has_effects(func) || args.iter().any(|a| self.expr_has_effects(a))
            }
            Expr::BinaryOp { left, right, .. } => {
                self.expr_has_effects(left) || self.expr_has_effects(right)
            }
            Expr::UnaryOp { operand, .. } => self.expr_has_effects(operand),
            Expr::If {
                condition,
                then_branch,
                else_branch,
                ..
            } => {
                self.expr_has_effects(condition)
                    || self.expr_has_effects(then_branch)
                    || self.expr_has_effects(else_branch)
            }
            Expr::Let { value, body, .. } => {
                self.expr_has_effects(value) || self.expr_has_effects(body)
            }
            Expr::Block {
                statements, result, ..
            } => {
                statements.iter().any(|s| match s {
                    Statement::Expr(e) => self.expr_has_effects(e),
                    Statement::Let { value, .. } => self.expr_has_effects(value),
                }) || self.expr_has_effects(result)
            }
            Expr::Lambda { body, .. } => self.expr_has_effects(body),
            Expr::Match { scrutinee, arms, .. } => {
                self.expr_has_effects(scrutinee)
                    || arms.iter().any(|a| self.expr_has_effects(&a.body))
            }
            Expr::Run { .. } => true,
            _ => false,
        }
    }

    fn check_function_naming(&mut self, f: &FunctionDecl) {
        let name = &f.name.name;
        // Functions should be camelCase (start with lowercase, no underscores except test_)
        if !name.starts_with("test_") && !name.starts_with('_') {
            if let Some(first) = name.chars().next() {
                if first.is_uppercase() {
                    self.emit(
                        LintId::NamingConvention,
                        f.name.span,
                        format!(
                            "function '{}' should start with a lowercase letter (camelCase)",
                            name
                        ),
                    );
                }
            }
        }
    }

    fn count_span_lines(&self, span: Span) -> usize {
        let start_line = self.source[..span.start].matches('\n').count();
        let end_line = self.source[..span.end.min(self.source.len())]
            .matches('\n')
            .count();
        end_line.saturating_sub(start_line) + 1
    }

    fn define_var(&mut self, name: &str) {
        if let Some(scope) = self.defined_vars.last_mut() {
            scope.insert(name.to_string());
        }
    }

    fn use_var(&mut self, name: &str) {
        self.used_vars.insert(name.to_string());
    }

    fn emit(&mut self, id: LintId, span: Span, message: String) {
        let level = self.config.level_for(id);
        if level == LintLevel::Allow {
            return;
        }
        self.lints.push(Lint {
            id,
            level,
            span,
            message,
            note: None,
            fix: None,
        });
    }
}

// ── Rendering ──────────────────────────────────────────────────────────────

/// Render lint results to a colored string
pub fn render_lints(lints: &[Lint], source: &str, filename: Option<&str>) -> String {
    let mut output = String::new();

    for lint in lints {
        let level_str = match lint.level {
            LintLevel::Deny => c(colors::RED, "error"),
            LintLevel::Warn => c(colors::YELLOW, "warning"),
            LintLevel::Allow => continue,
        };

        let lint_code = c(
            colors::DIM,
            &format!("[{}/{}]", lint.id.category().category_name(), lint.id.name()),
        );

        // Header: warning[idiom/could-be-pure]: message
        output.push_str(&format!("{}{}: {}\n", level_str, lint_code, lint.message));

        // Source location
        if lint.span.start > 0 || lint.span.end > 0 {
            let (line, col) = crate::diagnostics::offset_to_line_col(source, lint.span.start);
            let fname = filename.unwrap_or("<input>");
            output.push_str(&format!(
                "  {} {}:{}:{}\n",
                c(colors::CYAN, "-->"),
                fname,
                line,
                col
            ));

            // Show the source line
            if let Some(source_line) = crate::diagnostics::get_source_line(source, line) {
                let line_num = format!("{}", line);
                let padding = " ".repeat(line_num.len());
                output.push_str(&format!(
                    "  {} {}\n",
                    c(colors::DIM, &format!("{} |", padding)),
                    ""
                ));
                output.push_str(&format!(
                    "  {} {}\n",
                    c(colors::DIM, &format!("{} |", line_num)),
                    source_line
                ));

                // Underline
                let (_, end_col) =
                    crate::diagnostics::offset_to_line_col(source, lint.span.end.max(lint.span.start + 1));
                let underline_start = col.saturating_sub(1);
                let underline_len = end_col.saturating_sub(col).max(1);
                let underline_color = match lint.level {
                    LintLevel::Deny => colors::RED,
                    LintLevel::Warn => colors::YELLOW,
                    LintLevel::Allow => colors::DIM,
                };
                output.push_str(&format!(
                    "  {} {}{}\n",
                    c(colors::DIM, &format!("{} |", padding)),
                    " ".repeat(underline_start),
                    c(underline_color, &"^".repeat(underline_len)),
                ));
            }
        }

        // Note
        if let Some(note) = &lint.note {
            output.push_str(&format!("  {} {}\n", c(colors::DIM, "="), note));
        }

        // Explanation (for --explain mode)
        let explanation = lint.id.explanation();
        if !explanation.is_empty() {
            output.push_str(&format!("  {} {}\n", c(colors::CYAN, "="), c(colors::DIM, "help:")));
            for line in explanation.lines() {
                output.push_str(&format!("  {}   {}\n", c(colors::DIM, "|"), line));
            }
        }

        output.push('\n');
    }

    output
}

/// Render a summary line
pub fn render_summary(lints: &[Lint]) -> String {
    let errors = lints.iter().filter(|l| l.level == LintLevel::Deny).count();
    let warnings = lints.iter().filter(|l| l.level == LintLevel::Warn).count();

    if errors == 0 && warnings == 0 {
        return format!("{} No lint warnings", c(colors::GREEN, "\u{2713}"));
    }

    let mut parts = Vec::new();
    if errors > 0 {
        parts.push(c(colors::RED, &format!("{} errors", errors)));
    }
    if warnings > 0 {
        parts.push(c(colors::YELLOW, &format!("{} warnings", warnings)));
    }

    let icon = if errors > 0 {
        c(colors::RED, "\u{2717}")
    } else {
        c(colors::YELLOW, "\u{26a0}")
    };

    format!("{} {}", icon, parts.join(", "))
}

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

    fn lint_source(source: &str) -> Vec<Lint> {
        let program = Parser::parse_source(source).expect("parse failed");
        let config = LintConfig::default();
        Linter::new(config, source).lint(&program)
    }

    fn lint_source_with_config(source: &str, config: LintConfig) -> Vec<Lint> {
        let program = Parser::parse_source(source).expect("parse failed");
        Linter::new(config, source).lint(&program)
    }

    #[test]
    fn test_could_be_pure() {
        let lints = lint_source("fn add(a: Int, b: Int): Int = a + b");
        assert!(
            lints.iter().any(|l| l.id == LintId::CouldBePure),
            "expected could-be-pure lint, got: {:?}",
            lints.iter().map(|l| l.id.name()).collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_pure_function_no_lint() {
        let lints = lint_source("fn add(a: Int, b: Int): Int is pure = a + b");
        assert!(
            !lints.iter().any(|l| l.id == LintId::CouldBePure),
            "pure function should not trigger could-be-pure"
        );
    }

    #[test]
    fn test_single_arm_match() {
        let lints = lint_source(
            r#"
fn unwrap(x: Option<Int>): Int = match x {
    Some(v) => v
}
"#,
        );
        assert!(
            lints.iter().any(|l| l.id == LintId::SingleArmMatch),
            "expected single-arm-match lint, got: {:?}",
            lints.iter().map(|l| l.id.name()).collect::<Vec<_>>()
        );
    }

    #[test]
    fn test_naming_convention_uppercase_function() {
        let mut config = LintConfig::default();
        config
            .category_overrides
            .insert(LintCategory::Style, LintLevel::Warn);
        let lints = lint_source_with_config("fn MyFunc(): Int = 42", config);
        assert!(
            lints.iter().any(|l| l.id == LintId::NamingConvention),
            "expected naming-convention lint for uppercase function"
        );
    }

    #[test]
    fn test_too_many_params() {
        let mut config = LintConfig::default();
        config
            .category_overrides
            .insert(LintCategory::Style, LintLevel::Warn);
        config.max_params = 3;
        let lints = lint_source_with_config(
            "fn f(a: Int, b: Int, c: Int, d: Int): Int = a + b + c + d",
            config,
        );
        assert!(
            lints.iter().any(|l| l.id == LintId::TooManyParameters),
            "expected too-many-parameters lint"
        );
    }

    #[test]
    fn test_allow_suppresses_lint() {
        let mut config = LintConfig::default();
        config
            .lint_overrides
            .insert(LintId::CouldBePure, LintLevel::Allow);
        let lints = lint_source_with_config("fn add(a: Int, b: Int): Int = a + b", config);
        assert!(
            !lints.iter().any(|l| l.id == LintId::CouldBePure),
            "suppressed lint should not appear"
        );
    }

    #[test]
    fn test_test_functions_not_linted_for_pure() {
        let lints = lint_source("fn test_addition(): Bool = 2 + 2 == 4");
        assert!(
            !lints.iter().any(|l| l.id == LintId::CouldBePure),
            "test functions should not trigger could-be-pure"
        );
    }

    #[test]
    fn test_main_not_linted_for_pure() {
        let source = r#"fn main(): Unit with {Console} = {
    Console.print("hello")
}
let output = run main() with {}
"#;
        let lints = lint_source(source);
        assert!(
            !lints.iter().any(|l| l.id == LintId::CouldBePure),
            "main should not trigger could-be-pure"
        );
    }

    #[test]
    fn test_lint_config_category_override() {
        let mut config = LintConfig::default();
        config
            .category_overrides
            .insert(LintCategory::Idiom, LintLevel::Allow);
        let lints = lint_source_with_config("fn add(a: Int, b: Int): Int = a + b", config);
        assert!(
            !lints.iter().any(|l| l.id == LintId::CouldBePure),
            "idiom category should be suppressed"
        );
    }
}
