//! Helper functions for TypeScript/JavaScript analysis
//!
//! Provides complexity calculation and conversion utilities.

use crate::analyzers::typescript::parser::node_text;
use crate::analyzers::typescript::types::{FunctionKind, JsFunctionMetrics, NestedFunctionSummary};
use crate::complexity::entropy_core::{EntropyAnalysis, EntropyConfig};
use crate::complexity::threshold_manager::{ComplexityLevel, ComplexityThresholds, FunctionRole};
use crate::core::FunctionMetrics;
use tree_sitter::Node;

/// Calculate cyclomatic complexity for a function body
pub fn calculate_cyclomatic_complexity(node: &Node, source: &str) -> u32 {
    let mut complexity: u32 = 1; // Base complexity

    traverse_for_cyclomatic(node, source, &mut complexity);

    complexity
}

/// Recursively traverse nodes to count decision points
fn traverse_for_cyclomatic(node: &Node, source: &str, complexity: &mut u32) {
    if is_nested_function_boundary(node) {
        return;
    }

    match node.kind() {
        // Control flow statements
        "if_statement" => *complexity += 1,
        "for_statement" | "for_in_statement" => *complexity += 1,
        "while_statement" | "do_statement" => *complexity += 1,
        "switch_case" => {
            // Count each case except default
            if !is_default_case(node, source) {
                *complexity += 1;
            }
        }
        "catch_clause" => *complexity += 1,
        "ternary_expression" | "conditional_expression" => *complexity += 1,

        // Logical operators create branches
        "binary_expression" => {
            if let Some(op) = node.child_by_field_name("operator") {
                let op_text = node_text(&op, source);
                if op_text == "&&" || op_text == "||" {
                    *complexity += 1;
                }
            }
        }

        _ => {}
    }

    // Recurse into children
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        traverse_for_cyclomatic(&child, source, complexity);
    }
}

/// Check if a switch case is the default case
fn is_default_case(node: &Node, source: &str) -> bool {
    let text = node_text(node, source);
    text.trim_start().starts_with("default")
}

fn is_nested_function_boundary(node: &Node) -> bool {
    matches!(
        node.kind(),
        "arrow_function" | "function_expression" | "function"
    ) && node.child_by_field_name("body").is_some()
}

/// Calculate cognitive complexity for a function body
pub fn calculate_cognitive_complexity(node: &Node, source: &str) -> u32 {
    let mut complexity: u32 = 0;

    traverse_for_cognitive(node, source, &mut complexity, 0);

    complexity
}

/// Recursively traverse nodes for cognitive complexity
fn traverse_for_cognitive(node: &Node, source: &str, complexity: &mut u32, nesting: u32) {
    match node.kind() {
        // Control flow - adds 1 + nesting level
        "if_statement" => {
            *complexity += 1 + nesting;
            traverse_children_with_nesting(
                node,
                source,
                complexity,
                nesting + 1,
                Some("consequence"),
            );
            traverse_children_with_nesting(
                node,
                source,
                complexity,
                nesting + 1,
                Some("alternative"),
            );
            return;
        }
        "for_statement" | "for_in_statement" | "for_of_statement" => {
            *complexity += 1 + nesting;
            traverse_children_with_nesting(node, source, complexity, nesting + 1, Some("body"));
            return;
        }
        "while_statement" | "do_statement" => {
            *complexity += 1 + nesting;
            traverse_children_with_nesting(node, source, complexity, nesting + 1, Some("body"));
            return;
        }
        "switch_statement" => {
            *complexity += 1 + nesting;
            traverse_children_with_nesting(node, source, complexity, nesting + 1, Some("body"));
            return;
        }
        "try_statement" => {
            *complexity += 1 + nesting;
            // Try block
            if let Some(body) = node.child_by_field_name("body") {
                traverse_for_cognitive(&body, source, complexity, nesting + 1);
            }
            // Catch handler
            if let Some(handler) = node.child_by_field_name("handler") {
                traverse_for_cognitive(&handler, source, complexity, nesting + 1);
            }
            // Finally
            if let Some(finalizer) = node.child_by_field_name("finalizer") {
                traverse_for_cognitive(&finalizer, source, complexity, nesting + 1);
            }
            return;
        }
        "ternary_expression" | "conditional_expression" => {
            *complexity += 1 + nesting;
        }

        // Logical operators - add 1 (no nesting penalty for these)
        "binary_expression" => {
            if let Some(op) = node.child_by_field_name("operator") {
                let op_text = node_text(&op, source);
                if op_text == "&&" || op_text == "||" {
                    *complexity += 1;
                }
            }
        }

        // Await expressions add cognitive load
        "await_expression" => {
            *complexity += 1;
        }

        // Nested functions/callbacks increase complexity for the PARENT function.
        // We add complexity for the callback's presence but do NOT traverse into its body.
        // The callback's internal complexity is counted when analyzing the callback itself.
        "arrow_function" | "function_expression" | "function" => {
            // Callback presence adds complexity based on nesting level
            *complexity += 1 + nesting.min(2);
            // Don't traverse into body - avoids double-counting when callback is analyzed separately
            return;
        }
        // Top-level function declarations don't add complexity, just traverse body
        "function_declaration" | "generator_function_declaration" => {
            if let Some(body) = node.child_by_field_name("body") {
                traverse_for_cognitive(&body, source, complexity, nesting);
            }
            return;
        }

        _ => {}
    }

    // Continue recursion
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        traverse_for_cognitive(&child, source, complexity, nesting);
    }
}

/// Traverse children with optional field filter and nesting increment
fn traverse_children_with_nesting(
    node: &Node,
    source: &str,
    complexity: &mut u32,
    nesting: u32,
    field: Option<&str>,
) {
    // If a specific field is requested, only process that field
    if let Some(field_name) = field {
        if let Some(target) = node.child_by_field_name(field_name) {
            traverse_for_cognitive(&target, source, complexity, nesting);
        }
        // If field doesn't exist (e.g., no else branch), do nothing
        return;
    }

    // No field specified, process all children
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        traverse_for_cognitive(&child, source, complexity, nesting);
    }
}

/// Calculate maximum nesting depth
pub fn calculate_nesting_depth(node: &Node) -> u32 {
    calculate_nesting_recursive(node, 0)
}

fn calculate_nesting_recursive(node: &Node, current_depth: u32) -> u32 {
    if is_nested_function_boundary(node) {
        return current_depth;
    }

    // Only control flow structures add to nesting depth.
    // Functions/callbacks are NOT counted - their complexity is captured
    // separately in cognitive complexity, not nesting depth.
    let new_depth = match node.kind() {
        "if_statement" | "for_statement" | "for_in_statement" | "for_of_statement"
        | "while_statement" | "do_statement" | "switch_statement" | "try_statement" => {
            current_depth + 1
        }
        _ => current_depth,
    };

    let mut max_depth = new_depth;
    let mut cursor = node.walk();

    for child in node.children(&mut cursor) {
        let child_depth = calculate_nesting_recursive(&child, new_depth);
        max_depth = max_depth.max(child_depth);
    }

    max_depth
}

/// Summarize nested functions/callbacks excluded from parent complexity metrics.
pub fn summarize_nested_functions(node: &Node, source: &str) -> NestedFunctionSummary {
    let mut summary = NestedFunctionSummary::default();
    collect_nested_functions(node, source, &mut summary);
    summary
}

fn collect_nested_functions(node: &Node, source: &str, summary: &mut NestedFunctionSummary) {
    let mut cursor = node.walk();
    for child in node.children(&mut cursor) {
        if is_nested_function_boundary(&child) {
            add_nested_function_summary(&child, source, summary);
        } else {
            collect_nested_functions(&child, source, summary);
        }
    }
}

fn add_nested_function_summary(node: &Node, source: &str, summary: &mut NestedFunctionSummary) {
    summary.count += 1;

    if let Some(body) = node.child_by_field_name("body") {
        summary.cyclomatic += calculate_cyclomatic_complexity(&body, source);
        summary.cognitive += calculate_cognitive_complexity(&body, source);
        summary.max_nesting = summary.max_nesting.max(calculate_nesting_depth(&body));
        collect_nested_functions(&body, source, summary);
    }
}

/// Count lines in a function body
pub fn count_function_lines(node: &Node, _source: &str) -> usize {
    let start_line = node.start_position().row;
    let end_line = node.end_position().row;

    // Add 1 because lines are 0-indexed
    end_line - start_line + 1
}

/// Check if a function name indicates it's a test
pub fn is_test_function(name: &str) -> bool {
    let lower = name.to_lowercase();

    // Common test function patterns
    lower.starts_with("test")
        || lower.starts_with("it_")
        || lower.starts_with("should_")
        || lower.starts_with("spec_")
        || lower == "it"
        || lower == "test"
        || lower == "describe"
        || lower == "beforeeach"
        || lower == "aftereach"
        || lower == "beforeall"
        || lower == "afterall"
}

/// Convert JS-specific metrics to standard FunctionMetrics
pub fn convert_to_function_metrics(
    js_metrics: &JsFunctionMetrics,
    thresholds: &ComplexityThresholds,
) -> FunctionMetrics {
    use crate::core::PurityLevel;

    // Create entropy analysis from raw score if available
    let entropy_analysis = js_metrics
        .entropy_score
        .as_ref()
        .map(|raw| EntropyAnalysis::from_raw(raw, js_metrics.cognitive, &EntropyConfig::default()));

    // Convert purity level to is_pure boolean
    let is_pure = js_metrics
        .purity_level
        .map(|level| matches!(level, PurityLevel::StrictlyPure | PurityLevel::LocallyPure));

    // Format purity reason from impurity reasons
    let purity_reason = if js_metrics.impurity_reasons.is_empty() {
        None
    } else {
        Some(js_metrics.impurity_reasons.join(", "))
    };

    let mut metrics = FunctionMetrics {
        name: js_metrics.name.clone(),
        file: js_metrics.file.clone(),
        line: js_metrics.line,
        cyclomatic: js_metrics.cyclomatic,
        cognitive: js_metrics.cognitive,
        nesting: js_metrics.nesting,
        length: js_metrics.length,
        is_test: js_metrics.is_test,
        visibility: if js_metrics.is_exported {
            Some("export".to_string())
        } else {
            None
        },
        is_trait_method: false,
        in_test_module: js_metrics.is_test,
        entropy_score: js_metrics.entropy_score.clone(),
        is_pure,
        purity_confidence: js_metrics.purity_confidence,
        purity_reason,
        call_dependencies: None,
        upstream_callers: None,
        downstream_callees: None,
        mapping_pattern_result: None,
        adjusted_complexity: Some(calculate_adjusted_complexity(js_metrics, thresholds)),
        composition_metrics: None,
        language_specific: None,
        purity_level: js_metrics.purity_level,
        error_swallowing_count: None,
        error_swallowing_patterns: None,
        entropy_analysis,
        detected_patterns: None,
    };

    let detected_patterns = build_detected_patterns(js_metrics, thresholds, &metrics);
    metrics.detected_patterns = (!detected_patterns.is_empty()).then_some(detected_patterns);

    metrics
}

fn build_detected_patterns(
    js_metrics: &JsFunctionMetrics,
    thresholds: &ComplexityThresholds,
    metrics: &FunctionMetrics,
) -> Vec<String> {
    let mut patterns: Vec<String> = js_metrics
        .functional_chains
        .iter()
        .map(|chain| format!("functional-chain:{}", chain.methods.join("->")))
        .collect();
    patterns.extend(nested_function_patterns(&js_metrics.nested_functions));

    let role = classify_function_role(js_metrics);
    if thresholds.should_flag_function(metrics, role.clone()) {
        let level = match thresholds.get_complexity_level(metrics) {
            ComplexityLevel::Trivial => "trivial",
            ComplexityLevel::Moderate => "moderate",
            ComplexityLevel::High => "high",
            ComplexityLevel::Excessive => "excessive",
        };
        patterns.push(format!("threshold-exceeded:{level}"));
    }

    if metrics.cyclomatic >= thresholds.minimum_cyclomatic_complexity {
        patterns.push("cyclomatic-threshold-exceeded".to_string());
    }

    if metrics.cognitive >= thresholds.minimum_cognitive_complexity {
        patterns.push("cognitive-threshold-exceeded".to_string());
    }

    if metrics.length >= thresholds.minimum_function_length {
        patterns.push("length-threshold-exceeded".to_string());
    }

    patterns
}

fn nested_function_patterns(summary: &NestedFunctionSummary) -> Vec<String> {
    crate::complexity::nested_callable_patterns(&crate::complexity::NestedCallableSummary {
        count: summary.count,
        cyclomatic: summary.cyclomatic,
        cognitive: summary.cognitive,
        max_nesting: summary.max_nesting,
    })
}

fn classify_function_role(js_metrics: &JsFunctionMetrics) -> FunctionRole {
    if js_metrics.is_test {
        return FunctionRole::Test;
    }

    if js_metrics.name == "main"
        || js_metrics.name.ends_with("_handler")
        || js_metrics.name.starts_with("handle_")
    {
        return FunctionRole::EntryPoint;
    }

    if matches!(
        js_metrics.kind,
        FunctionKind::Getter | FunctionKind::Setter | FunctionKind::Constructor
    ) {
        return FunctionRole::Utility;
    }

    FunctionRole::from_name(
        js_metrics
            .name
            .rsplit("::")
            .next()
            .unwrap_or(&js_metrics.name),
    )
}

fn calculate_adjusted_complexity(
    js_metrics: &JsFunctionMetrics,
    thresholds: &ComplexityThresholds,
) -> f64 {
    let baseline = thresholds.minimum_total_complexity.max(1) as f64;
    (js_metrics.cyclomatic + js_metrics.cognitive) as f64 / baseline
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::analyzers::typescript::parser::parse_source;
    use crate::analyzers::typescript::types::FunctionKind;
    use crate::core::ast::JsLanguageVariant;
    use std::path::PathBuf;

    fn parse_and_get_body(source: &str) -> (tree_sitter::Tree, String) {
        let path = PathBuf::from("test.js");
        let ast = parse_source(source, &path, JsLanguageVariant::JavaScript).unwrap();
        (ast.tree, ast.source)
    }

    #[test]
    fn test_cyclomatic_simple_function() {
        let source = "function foo() { return 1; }";
        let (tree, source) = parse_and_get_body(source);
        let root = tree.root_node();

        let complexity = calculate_cyclomatic_complexity(&root, &source);
        assert_eq!(complexity, 1); // Base complexity
    }

    #[test]
    fn test_cyclomatic_with_if() {
        let source = "function foo(x) { if (x) { return 1; } return 0; }";
        let (tree, source) = parse_and_get_body(source);
        let root = tree.root_node();

        let complexity = calculate_cyclomatic_complexity(&root, &source);
        assert_eq!(complexity, 2); // 1 base + 1 if
    }

    #[test]
    fn test_cyclomatic_with_logical_operators() {
        let source = "function foo(a, b, c) { if (a && b || c) { return 1; } }";
        let (tree, source) = parse_and_get_body(source);
        let root = tree.root_node();

        let complexity = calculate_cyclomatic_complexity(&root, &source);
        assert_eq!(complexity, 4); // 1 base + 1 if + 2 logical ops
    }

    #[test]
    fn test_parent_cyclomatic_excludes_nested_callback_body() {
        let source = r#"
function useThing(flag) {
    if (flag) {
        return null;
    }
    const callback = () => {
        if (flag) {
            return 1;
        }
        return 0;
    };
    return callback;
}
"#;
        let (tree, source) = parse_and_get_body(source);
        let root = tree.root_node();

        assert_eq!(calculate_cyclomatic_complexity(&root, &source), 2);
    }

    #[test]
    fn test_nullish_defaults_do_not_inflate_async_persistence_complexity() {
        let source = r#"
async function persist(db, input) {
    await prepare(db);
    const artifact = input.artifact;
    const promptHash = artifact?.prompt ? await hash(artifact.prompt) : null;
    await db
        .prepare("insert into table values (?, ?, ?, ?, ?, ?)")
        .bind(
            artifact?.artifactId ?? null,
            artifact?.format ?? input.request?.format ?? null,
            artifact?.variant ?? input.request?.variant ?? null,
            input.request?.presetId ?? null,
            input.model ?? null,
            promptHash,
        )
        .run();
    return [
        artifact?.artifactId ?? null,
        artifact?.format ?? input.request?.format ?? null,
    ];
}
"#;
        let (tree, source) = parse_and_get_body(source);
        let root = tree.root_node();

        assert_eq!(calculate_cyclomatic_complexity(&root, &source), 2);
        assert_eq!(calculate_cognitive_complexity(&root, &source), 4);
    }

    #[test]
    fn test_cognitive_simple_function() {
        let source = "function foo() { return 1; }";
        let (tree, source) = parse_and_get_body(source);
        let root = tree.root_node();

        let complexity = calculate_cognitive_complexity(&root, &source);
        assert_eq!(complexity, 0);
    }

    #[test]
    fn test_cognitive_nested_if() {
        let source = r#"
function foo(a, b) {
    if (a) {
        if (b) {
            return 1;
        }
    }
}
"#;
        let (tree, source) = parse_and_get_body(source);
        let root = tree.root_node();

        let complexity = calculate_cognitive_complexity(&root, &source);
        // First if: 1 + 0 = 1, Second if: 1 + 1 = 2, Total: 3
        assert!(complexity >= 2);
    }

    #[test]
    fn test_nesting_depth() {
        let source = r#"
function foo(a, b, c) {
    if (a) {
        if (b) {
            if (c) {
                return 1;
            }
        }
    }
}
"#;
        let (tree, _source) = parse_and_get_body(source);
        let root = tree.root_node();

        let depth = calculate_nesting_depth(&root);
        assert_eq!(depth, 3);
    }

    #[test]
    fn test_parent_nesting_excludes_nested_callback_body() {
        let source = r#"
function useThing(flag) {
    if (flag) {
        return null;
    }
    const callback = () => {
        if (flag) {
            if (flag.ready) {
                return 1;
            }
        }
        return 0;
    };
    return callback;
}
"#;
        let (tree, _source) = parse_and_get_body(source);
        let root = tree.root_node();

        assert_eq!(calculate_nesting_depth(&root), 1);
    }

    #[test]
    fn test_summarize_nested_functions_preserves_callback_complexity() {
        let source = r#"
function useThing(flag) {
    const first = () => {
        if (flag) {
            return 1;
        }
        return 0;
    };
    const second = function(value) {
        return value ? 1 : 0;
    };
    return { first, second };
}
"#;
        let (tree, source) = parse_and_get_body(source);
        let root = tree.root_node();
        let summary = summarize_nested_functions(&root, &source);

        assert_eq!(summary.count, 2);
        assert_eq!(summary.cyclomatic, 4);
        assert_eq!(summary.cognitive, 2);
        assert_eq!(summary.max_nesting, 1);
    }

    #[test]
    fn test_is_test_function() {
        assert!(is_test_function("test_something"));
        assert!(is_test_function("testSomething"));
        assert!(is_test_function("it_should_work"));
        assert!(is_test_function("should_work"));
        assert!(is_test_function("describe"));
        assert!(is_test_function("beforeEach"));
        assert!(!is_test_function("regularFunction"));
        assert!(!is_test_function("getData"));
    }

    #[test]
    fn test_convert_to_function_metrics() {
        let js_metrics = JsFunctionMetrics::new(
            "test".to_string(),
            PathBuf::from("test.js"),
            10,
            FunctionKind::Declaration,
        );

        let metrics = convert_to_function_metrics(&js_metrics, &ComplexityThresholds::default());

        assert_eq!(metrics.name, "test");
        assert_eq!(metrics.line, 10);
        assert_eq!(metrics.cyclomatic, 1);
    }

    #[test]
    fn test_convert_to_function_metrics_honors_thresholds() {
        use crate::complexity::threshold_manager::ThresholdPreset;

        let mut js_metrics = JsFunctionMetrics::new(
            "handle_request".to_string(),
            PathBuf::from("test.ts"),
            5,
            FunctionKind::Declaration,
        );
        js_metrics.cyclomatic = 4;
        js_metrics.cognitive = 4;
        js_metrics.length = 18;

        let strict = ComplexityThresholds::from_preset(ThresholdPreset::Strict);
        let lenient = ComplexityThresholds::from_preset(ThresholdPreset::Lenient);

        let strict_metrics = convert_to_function_metrics(&js_metrics, &strict);
        let lenient_metrics = convert_to_function_metrics(&js_metrics, &lenient);

        assert!(
            strict_metrics
                .detected_patterns
                .as_ref()
                .is_some_and(|patterns| patterns
                    .iter()
                    .any(|pattern| pattern.starts_with("threshold-exceeded:"))),
            "strict thresholds should flag this function"
        );
        assert!(
            lenient_metrics
                .detected_patterns
                .as_ref()
                .is_none_or(|patterns| patterns
                    .iter()
                    .all(|pattern| !pattern.starts_with("threshold-exceeded:"))),
            "lenient thresholds should not flag this function"
        );
        assert_ne!(
            strict_metrics.adjusted_complexity,
            lenient_metrics.adjusted_complexity
        );
    }

    #[test]
    fn test_convert_to_function_metrics_preserves_nested_function_patterns() {
        let mut js_metrics = JsFunctionMetrics::new(
            "useThing".to_string(),
            PathBuf::from("test.ts"),
            5,
            FunctionKind::Declaration,
        );
        js_metrics.nested_functions = NestedFunctionSummary {
            count: 2,
            cyclomatic: 4,
            cognitive: 2,
            max_nesting: 1,
        };

        let metrics = convert_to_function_metrics(&js_metrics, &ComplexityThresholds::default());
        let patterns = metrics.detected_patterns.expect("nested patterns");

        assert!(patterns.contains(&"nested-functions:count=2".to_string()));
        assert!(patterns.contains(&"nested-functions:cyclomatic=4".to_string()));
        assert!(patterns.contains(&"nested-functions:cognitive=2".to_string()));
        assert!(patterns.contains(&"nested-functions:max-nesting=1".to_string()));
    }
}
