//! Cyclomatic complexity calculation for Rust code blocks.
//!
//! **Note**: For new code, prefer using [`super::pure::calculate_cyclomatic_pure`]
//! which operates directly on `syn::File` AST and is faster to test.
//! This module provides block-level complexity calculation which is still
//! useful for analyzing individual function bodies.
//!
//! The pure functions in `pure.rs` are:
//! - Deterministic (same input = same output)
//! - Fast to test (no I/O overhead)
//! - Easier to compose
//!
//! See [`super::pure`] for file-level pure functions.

use super::match_patterns::detect_match_expression;
use syn::{Block, Expr, Stmt};

enum WorkItem<'ast> {
    Expr(&'ast Expr, bool),
    Stmt(&'ast Stmt),
}

struct TraversalState<'ast> {
    complexity: u32,
    work: Vec<WorkItem<'ast>>,
}

impl<'ast> TraversalState<'ast> {
    fn from_block(block: &'ast Block) -> Self {
        let mut traversal = Self {
            complexity: 1,
            work: Vec::new(),
        };
        traversal.push_block(block);
        traversal
    }

    fn run(mut self) -> u32 {
        while let Some(item) = self.work.pop() {
            self.process_item(item);
        }

        self.complexity
    }

    fn process_item(&mut self, item: WorkItem<'ast>) {
        match item {
            WorkItem::Expr(expr, in_condition) => self.process_expr(expr, in_condition),
            WorkItem::Stmt(stmt) => self.process_stmt(stmt),
        }
    }

    fn process_stmt(&mut self, stmt: &'ast Stmt) {
        if let Some(expr) = stmt_expr(stmt) {
            self.push_expr(expr, false);
        }
    }

    fn process_expr(&mut self, expr: &'ast Expr, in_condition: bool) {
        self.complexity += calculate_expr_complexity(expr, in_condition);
        push_expr_children(self, expr, in_condition);
    }

    fn push_expr(&mut self, expr: &'ast Expr, in_condition: bool) {
        self.work.push(WorkItem::Expr(expr, in_condition));
    }

    fn push_stmt(&mut self, stmt: &'ast Stmt) {
        self.work.push(WorkItem::Stmt(stmt));
    }

    fn push_block(&mut self, block: &'ast Block) {
        for stmt in block.stmts.iter().rev() {
            self.push_stmt(stmt);
        }
    }
}

/// Calculate cyclomatic complexity using iterative AST traversal.
///
/// Uses an explicit stack instead of recursive visitor to avoid stack overflow
/// on deeply nested AST structures.
pub fn calculate_cyclomatic(block: &Block) -> u32 {
    TraversalState::from_block(block).run()
}

fn stmt_expr(stmt: &Stmt) -> Option<&Expr> {
    match stmt {
        Stmt::Local(local) => local.init.as_ref().map(|init| init.expr.as_ref()),
        Stmt::Expr(expr, _) => Some(expr),
        Stmt::Item(_) | Stmt::Macro(_) => None,
    }
}

fn push_expr_children<'ast>(
    traversal: &mut TraversalState<'ast>,
    expr: &'ast Expr,
    in_condition: bool,
) {
    if push_control_flow_children(traversal, expr) {
        return;
    }

    if push_call_and_access_children(traversal, expr) {
        return;
    }

    if push_operator_children(traversal, expr, in_condition) {
        return;
    }

    let _ = push_literal_children(traversal, expr);
}

fn push_control_flow_children<'ast>(
    traversal: &mut TraversalState<'ast>,
    expr: &'ast Expr,
) -> bool {
    match expr {
        Expr::If(expr_if) => {
            push_optional_expr(
                traversal,
                expr_if.else_branch.as_ref().map(|(_, expr)| expr.as_ref()),
            );
            traversal.push_block(&expr_if.then_branch);
            traversal.push_expr(&expr_if.cond, true);
            true
        }
        Expr::While(expr_while) => {
            traversal.push_block(&expr_while.body);
            traversal.push_expr(&expr_while.cond, true);
            true
        }
        Expr::ForLoop(expr_for) => {
            traversal.push_block(&expr_for.body);
            traversal.push_expr(&expr_for.expr, false);
            true
        }
        Expr::Loop(expr_loop) => {
            traversal.push_block(&expr_loop.body);
            true
        }
        Expr::Match(expr_match) => {
            for arm in expr_match.arms.iter().rev() {
                traversal.push_expr(&arm.body, false);
                push_optional_expr(
                    traversal,
                    arm.guard.as_ref().map(|(_, guard)| guard.as_ref()),
                );
            }
            traversal.push_expr(&expr_match.expr, false);
            true
        }
        Expr::Block(expr_block) => {
            traversal.push_block(&expr_block.block);
            true
        }
        Expr::Closure(closure) => {
            traversal.push_expr(&closure.body, false);
            true
        }
        Expr::Async(async_block) => {
            traversal.push_block(&async_block.block);
            true
        }
        Expr::Try(expr_try) => {
            traversal.push_expr(&expr_try.expr, false);
            true
        }
        _ => false,
    }
}

fn push_call_and_access_children<'ast>(
    traversal: &mut TraversalState<'ast>,
    expr: &'ast Expr,
) -> bool {
    match expr {
        Expr::Call(call) => {
            for arg in call.args.iter().rev() {
                traversal.push_expr(arg, false);
            }
            traversal.push_expr(&call.func, false);
            true
        }
        Expr::MethodCall(method_call) => {
            for arg in method_call.args.iter().rev() {
                traversal.push_expr(arg, false);
            }
            traversal.push_expr(&method_call.receiver, false);
            true
        }
        Expr::Field(field) => {
            traversal.push_expr(&field.base, false);
            true
        }
        Expr::Index(index) => {
            traversal.push_expr(&index.index, false);
            traversal.push_expr(&index.expr, false);
            true
        }
        Expr::Await(await_expr) => {
            traversal.push_expr(&await_expr.base, false);
            true
        }
        _ => false,
    }
}

fn push_operator_children<'ast>(
    traversal: &mut TraversalState<'ast>,
    expr: &'ast Expr,
    in_condition: bool,
) -> bool {
    match expr {
        Expr::Binary(binary) => {
            traversal.push_expr(&binary.right, in_condition);
            traversal.push_expr(&binary.left, in_condition);
            true
        }
        Expr::Unary(unary) => {
            traversal.push_expr(&unary.expr, in_condition);
            true
        }
        Expr::Paren(paren) => {
            traversal.push_expr(&paren.expr, in_condition);
            true
        }
        Expr::Reference(reference) => {
            traversal.push_expr(&reference.expr, false);
            true
        }
        Expr::Cast(cast) => {
            traversal.push_expr(&cast.expr, false);
            true
        }
        Expr::Assign(assign) => {
            traversal.push_expr(&assign.right, false);
            traversal.push_expr(&assign.left, false);
            true
        }
        Expr::Let(let_expr) => {
            traversal.push_expr(&let_expr.expr, false);
            true
        }
        _ => false,
    }
}

fn push_literal_children<'ast>(traversal: &mut TraversalState<'ast>, expr: &'ast Expr) -> bool {
    match expr {
        Expr::Return(ret) => {
            push_optional_expr(traversal, ret.expr.as_deref());
            true
        }
        Expr::Break(brk) => {
            push_optional_expr(traversal, brk.expr.as_deref());
            true
        }
        Expr::Tuple(tuple) => {
            for elem in tuple.elems.iter().rev() {
                traversal.push_expr(elem, false);
            }
            true
        }
        Expr::Array(array) => {
            for elem in array.elems.iter().rev() {
                traversal.push_expr(elem, false);
            }
            true
        }
        Expr::Struct(struct_expr) => {
            push_optional_expr(traversal, struct_expr.rest.as_deref());
            for field in struct_expr.fields.iter().rev() {
                traversal.push_expr(&field.expr, false);
            }
            true
        }
        Expr::Repeat(repeat) => {
            traversal.push_expr(&repeat.len, false);
            traversal.push_expr(&repeat.expr, false);
            true
        }
        Expr::Range(range) => {
            push_optional_expr(traversal, range.end.as_deref());
            push_optional_expr(traversal, range.start.as_deref());
            true
        }
        Expr::Yield(yield_expr) => {
            push_optional_expr(traversal, yield_expr.expr.as_deref());
            true
        }
        _ => false,
    }
}

fn push_optional_expr<'ast>(traversal: &mut TraversalState<'ast>, expr: Option<&'ast Expr>) {
    if let Some(expr) = expr {
        traversal.push_expr(expr, false);
    }
}

/// Calculate cyclomatic complexity with pattern adjustments
///
/// When a pattern-like match expression is detected, its contribution to
/// complexity is adjusted using logarithmic scaling. Importantly, this
/// adjustment only affects the match's contribution - other control flow
/// in the block is preserved.
pub fn calculate_cyclomatic_adjusted(block: &Block) -> u32 {
    let base = calculate_cyclomatic(block);

    // Check for match expressions that should use logarithmic scaling
    for stmt in &block.stmts {
        if let Stmt::Expr(expr, _) = stmt {
            if let Some(info) = detect_match_expression(expr) {
                // Calculate the match's original contribution: (arms - 1)
                let original_match_contribution = info.condition_count.saturating_sub(1) as u32;

                // Calculate the adjusted contribution using logarithmic scaling
                let adjusted_match = (info.condition_count as f32).log2().ceil() as u32;
                let default_penalty = if !info.has_default { 1 } else { 0 };

                // Replace just the match's contribution, preserving other complexity
                return base - original_match_contribution + adjusted_match + default_penalty;
            }
        }
    }

    // Check for if-else pattern matching using existing pattern recognizers
    use super::pattern_adjustments::{PatternMatchRecognizer, PatternRecognizer};
    let recognizer = PatternMatchRecognizer::new();
    if let Some(info) = recognizer.detect(block) {
        return recognizer.adjust_complexity(&info, base);
    }

    base
}

/// Calculate complexity contribution for a single expression.
///
/// McCabe cyclomatic complexity counts decision points (predicates), not branches.
/// An if-else has ONE decision point regardless of whether there's an else branch.
/// The else branch is just the alternative path from the single decision.
///
/// Logical operators (&&, ||) always add complexity because they represent
/// additional predicates that can independently affect control flow.
/// `if a && b && c` has 3 predicates and is more complex than `if a`.
fn calculate_expr_complexity(expr: &Expr, _in_condition: bool) -> u32 {
    match expr {
        // If adds 1 decision point regardless of else branch presence.
        // The else is not a separate decision - it's the alternative path.
        Expr::If(_) => 1,
        Expr::While(_) | Expr::ForLoop(_) | Expr::Loop(_) => 1,
        Expr::Try(_) => 1,
        Expr::Match(expr_match) => expr_match.arms.len().saturating_sub(1) as u32,
        // Logical operators always add complexity - they represent additional predicates
        // that can independently affect control flow, regardless of context.
        Expr::Binary(binary) if is_logical_operator(&binary.op) => 1,
        _ => 0,
    }
}

fn is_logical_operator(op: &syn::BinOp) -> bool {
    matches!(op, syn::BinOp::And(_) | syn::BinOp::Or(_))
}

pub fn calculate_cyclomatic_for_function(complexity: u32, params: usize) -> u32 {
    complexity + params.saturating_sub(1) as u32
}

pub fn combine_cyclomatic(branches: Vec<u32>) -> u32 {
    branches.iter().sum::<u32>() + 1
}

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

    /// Bug fix test: if-else should count as 1 decision point, not 2.
    /// McCabe cyclomatic complexity counts decision points (predicates),
    /// not branches. An if-else has ONE decision point regardless of
    /// whether there's an else branch.
    #[test]
    fn test_if_else_counts_as_one_decision_point() {
        // Single if without else: complexity = 1 (base) + 1 (if) = 2
        let block_if_only: Block = parse_quote! {{
            if x > 0 {
                do_something();
            }
        }};
        assert_eq!(
            calculate_cyclomatic(&block_if_only),
            2,
            "if without else should add 1 to base complexity"
        );

        // Single if WITH else: complexity should ALSO be 1 (base) + 1 (if) = 2
        // The else is not a separate decision point - it's the alternative path
        let block_if_else: Block = parse_quote! {{
            if x > 0 {
                do_something();
            } else {
                do_other();
            }
        }};
        assert_eq!(
            calculate_cyclomatic(&block_if_else),
            2,
            "if-else should add 1 to base complexity, not 2 (else is not a decision point)"
        );
    }

    #[test]
    fn test_multiple_if_else_chains() {
        // 3 sequential if-else statements: complexity = 1 (base) + 3 (ifs) = 4
        let block: Block = parse_quote! {{
            if a { x(); } else { y(); }
            if b { x(); } else { y(); }
            if c { x(); } else { y(); }
        }};
        assert_eq!(
            calculate_cyclomatic(&block),
            4,
            "3 if-else statements should add 3 to base complexity"
        );
    }

    #[test]
    fn test_nested_if_else() {
        // Nested if-else: complexity = 1 (base) + 2 (two if decisions) = 3
        let block: Block = parse_quote! {{
            if a {
                if b {
                    x();
                } else {
                    y();
                }
            } else {
                z();
            }
        }};
        assert_eq!(
            calculate_cyclomatic(&block),
            3,
            "nested if-else should count 2 decisions (outer if + inner if)"
        );
    }

    #[test]
    fn test_match_complexity() {
        // Match with 3 arms: complexity = 1 (base) + 2 (arms - 1) = 3
        let block: Block = parse_quote! {{
            match x {
                A => 1,
                B => 2,
                _ => 3,
            }
        }};
        assert_eq!(
            calculate_cyclomatic(&block),
            3,
            "match with 3 arms should add 2 (arms - 1) to base complexity"
        );
    }

    #[test]
    fn test_loop_complexity() {
        let block: Block = parse_quote! {{
            while condition {
                do_work();
            }
            for i in items {
                process(i);
            }
            loop {
                if done { break; }
            }
        }};
        // 1 (base) + 1 (while) + 1 (for) + 1 (loop) + 1 (if inside loop) = 5
        assert_eq!(calculate_cyclomatic(&block), 5);
    }

    /// Bug fix test: Logical operators inside conditions SHOULD add complexity.
    /// `if a && b && c` has 3 predicates and should have higher complexity
    /// than `if a`.
    #[test]
    fn test_logical_operators_in_conditions_add_complexity() {
        // Single condition: complexity = 1 (base) + 1 (if) = 2
        let single_condition: Block = parse_quote! {{
            if a {
                do_something();
            }
        }};
        assert_eq!(
            calculate_cyclomatic(&single_condition),
            2,
            "Single condition should have complexity 2"
        );

        // Multiple conditions with &&: complexity = 1 (base) + 1 (if) + 2 (&& operators) = 4
        let three_conditions: Block = parse_quote! {{
            if a && b && c {
                do_something();
            }
        }};
        assert!(
            calculate_cyclomatic(&three_conditions) > 2,
            "if a && b && c should have higher complexity than if a (got {})",
            calculate_cyclomatic(&three_conditions)
        );

        // Mixed && and ||: should also add complexity
        let mixed_operators: Block = parse_quote! {{
            if a && b || c {
                do_something();
            }
        }};
        assert!(
            calculate_cyclomatic(&mixed_operators) > 2,
            "if a && b || c should have higher complexity than if a (got {})",
            calculate_cyclomatic(&mixed_operators)
        );
    }

    #[test]
    fn test_traverses_closure_and_async_children() {
        let block: Block = parse_quote! {{
            let closure = || if ready { work() } else { wait() };
            async {
                while pending {
                    poll().await;
                }
            };
        }};

        assert_eq!(
            calculate_cyclomatic(&block),
            3,
            "closure if and async while should both contribute complexity"
        );
    }

    #[test]
    fn test_traverses_nested_literal_children() {
        let block: Block = parse_quote! {{
            let value = Config {
                enabled: if flag { true } else { false },
                ..default_config()
            };
            let repeated = [if value.enabled { 1 } else { 0 }; if count > 0 { count } else { 1 }];
            let range = (if start_ok { start } else { 0 })..(if end_ok { end } else { 1 });
        }};

        assert_eq!(
            calculate_cyclomatic(&block),
            6,
            "struct fields, repeat expressions, and range bounds should be traversed"
        );
    }

    /// Bug fix test: calculate_cyclomatic_adjusted should NOT discard base complexity
    /// when a pattern match is detected. The adjustment should only affect the
    /// match expression's contribution, not the entire function's complexity.
    #[test]
    fn test_adjusted_preserves_other_complexity() {
        // Function with control flow BEFORE a match expression
        let block: Block = parse_quote! {{
            if condition {
                do_something();
            }
            for i in items {
                process(i);
            }
            match x {
                A => 1,
                B => 2,
                C => 3,
                D => 4,
                E => 5,
                F => 6,
                G => 7,
                _ => 8,
            }
        }};

        let base = calculate_cyclomatic(&block);
        let adjusted = calculate_cyclomatic_adjusted(&block);

        // Base should be: 1 (base) + 1 (if) + 1 (for) + 7 (match: 8 arms - 1) = 10
        assert_eq!(base, 10, "Base complexity should include all control flow");

        // Adjusted should NOT be just log2(8)=3 - it should preserve the if and for loop
        // The if and for contribute 2, and the match should be adjusted to ~3
        // So adjusted should be around 1 (base) + 1 (if) + 1 (for) + 3 (adjusted match) = 6
        // At minimum, it should be > 3 (the match-only adjustment)
        assert!(
            adjusted > 3,
            "Adjusted complexity ({}) should preserve non-match control flow, not just return match adjustment",
            adjusted
        );
    }
}
