//! Legolas - The Code Analyzer Agent
//!
//! Legolas performs static analysis on source code.  It computes quality
//! metrics (lines of code, estimated cyclomatic complexity, etc.), detects
//! common issues (unused imports, hardcoded secrets, TODO markers, deeply
//! nested logic, long functions), and produces human-readable reports.
//!
//! Unlike the LLD design doc (which assumed tree-sitter), this implementation
//! works purely with regex-based heuristics so that it compiles with the
//! existing Cargo dependencies (regex, etc.) and requires no additional
//! native parser libraries.

use std::sync::Arc;

use anyhow::Result;
use regex::Regex;

use crate::db::kuzu::KuzuGraph;
use crate::db::sqlite::SqliteDb;
use crate::types::{
    AnalysisIssue, AnalysisMetrics, CodeAnalysis, LlmProvider, Severity,
};

// ---------------------------------------------------------------------------
// Constants
// ---------------------------------------------------------------------------

/// Lines-of-code threshold above which a single function is considered "long".
const LONG_FUNCTION_LOC_THRESHOLD: usize = 50;

/// Nesting depth at which we emit a warning.
const HIGH_NESTING_THRESHOLD: usize = 4;

/// Maximum parameter count before we flag a function.
const MAX_RECOMMENDED_PARAMS: usize = 5;

/// Cyclomatic-complexity estimate above which we emit a warning.
/// TODO: Wire into `detect_complexity_issues` to flag high-complexity functions.
const _HIGH_COMPLEXITY_THRESHOLD: f64 = 15.0;

// ---------------------------------------------------------------------------
// Legolas
// ---------------------------------------------------------------------------

/// Legolas analyses source code for quality, correctness, and security issues.
///
/// Two analysis tiers:
/// 1. **Heuristic** (always runs): regex-based detection of common issues —
///    unused imports, secrets, TODOs, long functions, deep nesting, etc.
/// 2. **LLM-powered** (when available): deep code review via MCP sampling for
///    logic errors, design issues, security flaws, and correctness problems
///    that heuristics cannot catch.
pub struct Legolas {
    /// Knowledge graph for dependency and structural queries.
    kuzu: Arc<KuzuGraph>,
    /// SQLite handle for persisting analysis results and reading task data.
    _sqlite: Arc<SqliteDb>,
    /// Optional LLM provider for deep code review.
    llm: Option<Arc<dyn LlmProvider>>,
}

impl Legolas {
    // -- Construction -------------------------------------------------------

    pub fn new(kuzu: Arc<KuzuGraph>, sqlite: Arc<SqliteDb>) -> Self {
        Self { kuzu, _sqlite: sqlite, llm: None }
    }

    pub fn with_llm(
        kuzu: Arc<KuzuGraph>,
        sqlite: Arc<SqliteDb>,
        llm: Arc<dyn LlmProvider>,
    ) -> Self {
        Self { kuzu, _sqlite: sqlite, llm: Some(llm) }
    }

    // -- Public API ---------------------------------------------------------

    /// Full analysis: heuristic checks + LLM-powered deep review (when
    /// available).  Used by the `analyze_file` MCP tool for single-file
    /// analysis where one LLM call is acceptable.
    pub async fn analyze_file(
        &self,
        file_path: &str,
        content: &str,
    ) -> Result<CodeAnalysis> {
        let mut analysis = self.analyze_file_heuristic(file_path, content).await?;

        // Tier 2: LLM-powered deep review (when available).
        if let Some(ref llm) = self.llm.as_ref().filter(|l| l.is_available()) {
            match self.llm_review(llm, file_path, content).await {
                Ok(llm_issues) => {
                    tracing::info!(
                        file = %file_path,
                        llm_issues = llm_issues.len(),
                        "Legolas: LLM review complete"
                    );
                    analysis.issues.extend(llm_issues);
                    analysis.issues.sort_by_key(|i| i.line);
                }
                Err(e) => {
                    tracing::warn!(
                        file = %file_path,
                        error = %e,
                        "Legolas: LLM review failed, using heuristic results only"
                    );
                }
            }
        }

        Ok(analysis)
    }

    /// Heuristic-only analysis: regex-based issue detection + metrics +
    /// KuzuGraph dependency enrichment.  No LLM call.
    ///
    /// Used by AutoForge for concurrent per-file analysis (the LLM review
    /// is handled separately via [`review_code_batch`] for efficiency).
    pub async fn analyze_file_heuristic(
        &self,
        file_path: &str,
        content: &str,
    ) -> Result<CodeAnalysis> {
        tracing::info!(file = %file_path, "Legolas: analysing file (heuristic)");

        let metrics = self.compute_metrics(content);
        let issues = self.detect_issues(content, file_path);

        // Enrich with graph dependency info when available.
        let dep_count = match tokio::time::timeout(
            std::time::Duration::from_secs(5),
            self.kuzu.get_dependencies(file_path),
        )
        .await
        {
            Ok(Ok(deps)) => deps.len(),
            Ok(Err(_)) => 0,
            Err(_) => {
                tracing::warn!(file = %file_path, "KuzuGraph dependency query timed out after 5s");
                0
            }
        };

        let metrics = AnalysisMetrics {
            dependency_count: dep_count,
            ..metrics
        };

        tracing::info!(
            file = %file_path,
            issues = issues.len(),
            complexity = metrics.complexity,
            "Legolas: heuristic analysis complete"
        );

        Ok(CodeAnalysis { issues, metrics })
    }

    /// LLM-powered code review for a single file.
    ///
    /// Returns additional issues found by the LLM that heuristics miss:
    /// logic errors, design problems, security flaws, incorrect error handling.
    async fn llm_review(
        &self,
        llm: &Arc<dyn LlmProvider>,
        file_path: &str,
        content: &str,
    ) -> Result<Vec<AnalysisIssue>> {
        let system = "You are Legolas, an expert code reviewer inside the FURNACE system.\n\
            Review the code for: logic errors, security vulnerabilities, incorrect error handling, \
            design issues, and potential bugs that static analysis cannot catch.\n\
            Respond ONLY as a JSON array of objects, each with:\n\
            {\"line\": <number>, \"severity\": \"error\"|\"warning\"|\"info\", \"message\": \"...\"}\n\
            If no issues found, respond with an empty array: []\n\
            IMPORTANT: Respond with JSON directly. Do not ask questions.";

        let prompt = format!(
            "Review this file: `{}`\n\n```\n{}\n```",
            file_path, content
        );

        let raw = tokio::time::timeout(
            std::time::Duration::from_secs(60),
            llm.complete_with_system(system, &prompt),
        )
        .await
        .map_err(|_| anyhow::anyhow!("LLM review timed out after 60s"))?
        .map_err(|e| anyhow::anyhow!("LLM review failed: {e}"))?;

        Self::parse_llm_review_issues(&raw, file_path)
    }

    /// Batch LLM code review for multiple files in a single call.
    ///
    /// Returns a vec of `(file_path, Vec<AnalysisIssue>)`.  Falls back to
    /// individual reviews if batch parsing fails.
    pub async fn review_code_batch(
        &self,
        files: &[(&str, &str)], // (file_path, content)
    ) -> Result<Vec<(String, Vec<AnalysisIssue>)>> {
        let llm = match self.llm.as_ref().filter(|l| l.is_available()) {
            Some(llm) => llm,
            None => return Ok(vec![]),
        };

        if files.is_empty() {
            return Ok(vec![]);
        }

        // Single file — use individual review.
        if files.len() == 1 {
            let issues = self.llm_review(llm, files[0].0, files[0].1).await?;
            return Ok(vec![(files[0].0.to_string(), issues)]);
        }

        tracing::info!(count = files.len(), "Legolas: batch LLM code review starting");

        let system = format!(
            "You are Legolas, an expert code reviewer inside the FURNACE system.\n\
            You are reviewing {} files simultaneously.\n\
            For EACH file, output a JSON array of issues inside a fenced code block.\n\
            Output exactly {} fenced code blocks, one per file, in the same order as given.\n\
            Each block contains: [{{\"line\": N, \"severity\": \"error\"|\"warning\"|\"info\", \"message\": \"...\"}}]\n\
            If a file has no issues, output an empty array: []\n\
            IMPORTANT: Respond with JSON directly. Do not ask questions.",
            files.len(),
            files.len()
        );

        let mut prompt_parts = Vec::new();
        for (i, (path, content)) in files.iter().enumerate() {
            prompt_parts.push(format!(
                "## File {} of {}: `{}`\n```\n{}\n```",
                i + 1,
                files.len(),
                path,
                content,
            ));
        }
        let prompt = prompt_parts.join("\n\n---\n\n");

        let raw = tokio::time::timeout(
            std::time::Duration::from_secs(180),
            llm.complete_with_system(&system, &prompt),
        )
        .await
        .map_err(|_| anyhow::anyhow!("Batch LLM review timed out after 180s"))?
        .map_err(|e| anyhow::anyhow!("Batch LLM review failed: {e}"))?;

        // Positional parsing: extract fenced code blocks in order.
        let blocks = Self::extract_all_code_blocks(&raw);
        if blocks.len() == files.len() {
            let mut results = Vec::new();
            for ((path, _), block) in files.iter().zip(blocks.iter()) {
                let issues = Self::parse_llm_review_issues(block, path).unwrap_or_default();
                results.push((path.to_string(), issues));
            }
            tracing::info!(count = results.len(), "Legolas: batch LLM review complete");
            return Ok(results);
        }

        // Fallback: individual reviews.
        tracing::warn!("Batch review parsing failed, falling back to individual reviews");
        let mut results = Vec::new();
        for (path, content) in files {
            match self.llm_review(llm, path, content).await {
                Ok(issues) => results.push((path.to_string(), issues)),
                Err(e) => {
                    tracing::warn!(file = %path, error = %e, "Individual LLM review fallback failed");
                    results.push((path.to_string(), vec![]));
                }
            }
        }
        Ok(results)
    }

    /// Parse JSON issues from an LLM review response.
    pub fn parse_llm_review_issues(raw: &str, file_path: &str) -> Result<Vec<AnalysisIssue>> {
        // Strip markdown fences if present.
        let cleaned = raw.trim();
        let json_str = if cleaned.starts_with("```") {
            let after_first = cleaned.find('\n').map(|i| &cleaned[i + 1..]).unwrap_or(cleaned);
            after_first.rfind("```").map(|i| &after_first[..i]).unwrap_or(after_first).trim()
        } else {
            cleaned
        };

        let items: Vec<serde_json::Value> = serde_json::from_str(json_str)
            .map_err(|e| anyhow::anyhow!("Failed to parse LLM review JSON: {e}"))?;

        let mut issues = Vec::new();
        for item in items {
            let line = item.get("line").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
            let severity_str = item.get("severity").and_then(|v| v.as_str()).unwrap_or("warning");
            let severity = match severity_str {
                "error" => Severity::Error,
                "info" => Severity::Info,
                _ => Severity::Warning,
            };
            let message = item.get("message").and_then(|v| v.as_str()).unwrap_or("").to_string();
            if !message.is_empty() {
                issues.push(AnalysisIssue {
                    file_path: file_path.to_string(),
                    line,
                    severity,
                    message,
                });
            }
        }

        Ok(issues)
    }

    /// Extract all fenced code blocks from a response, in order.
    fn extract_all_code_blocks(response: &str) -> Vec<String> {
        let mut blocks = Vec::new();
        let mut lines = response.lines().peekable();

        while let Some(line) = lines.next() {
            if line.starts_with("```") {
                let mut code_lines = Vec::new();
                for inner_line in lines.by_ref() {
                    if inner_line.starts_with("```") {
                        break;
                    }
                    code_lines.push(inner_line);
                }
                let code = code_lines.join("\n");
                if !code.trim().is_empty() {
                    blocks.push(code);
                }
            }
        }

        blocks
    }

    /// Build prefetch prompts for code review without calling the LLM.
    ///
    /// Returns one `PrefetchPrompt` per file. Each prompt contains the same
    /// system + user prompt that `llm_review` would use.
    pub fn prepare_review_prompts(
        &self,
        files: &[(&str, &str)], // (file_path, content)
    ) -> Vec<crate::mcp::prefetch::PrefetchPrompt> {
        let system_prompt = "You are Legolas, an expert code reviewer inside the FURNACE system.\n\
            Review the code for: logic errors, security vulnerabilities, incorrect error handling, \
            design issues, and potential bugs that static analysis cannot catch.\n\
            Respond ONLY as a JSON array of objects, each with:\n\
            {\"line\": <number>, \"severity\": \"error\"|\"warning\"|\"info\", \"message\": \"...\"}\n\
            If no issues found, respond with an empty array: []\n\
            IMPORTANT: Respond with JSON directly. Do not ask questions.".to_string();

        files
            .iter()
            .map(|(file_path, content)| {
                let user_prompt = format!(
                    "Review this file: `{}`\n\n```\n{}\n```",
                    file_path, content
                );
                // Encode file path reversibly: replace '/' with '::' (which
                // won't appear in real file paths).  Dots are preserved.
                let encoded_path = file_path.replace('/', "::");
                crate::mcp::prefetch::PrefetchPrompt {
                    id: format!("legolas-review-{}", encoded_path),
                    agent: "Legolas".into(),
                    task_id: Some(file_path.to_string()),
                    system_prompt: system_prompt.clone(),
                    user_prompt,
                    description: format!("Code review for {}", file_path),
                }
            })
            .collect()
    }

    /// Compute aggregate metrics for *content*.
    ///
    /// Metrics include:
    /// - **complexity**: Estimated cyclomatic complexity (counts branch
    ///   keywords).
    /// - **test_coverage**: A heuristic estimate based on the ratio of test
    ///   functions to total public functions.  0.0 when no test data is
    ///   available.
    /// - **dependency_count**: Set to 0 here; callers may enrich via the
    ///   knowledge graph.
    pub fn compute_metrics(&self, content: &str) -> AnalysisMetrics {
        let complexity = Self::estimate_cyclomatic_complexity(content);
        let test_coverage = Self::estimate_test_coverage(content);

        AnalysisMetrics {
            complexity,
            test_coverage,
            dependency_count: 0,
        }
    }

    /// Detect issues in *content*.  Returns a (possibly empty) vec of
    /// `AnalysisIssue` sorted by line number.
    pub fn detect_issues(&self, content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let mut issues = Vec::new();

        // Run all detectors.
        issues.extend(Self::detect_unused_imports(content, file_path));
        issues.extend(Self::detect_hardcoded_secrets(content, file_path));
        issues.extend(Self::detect_todo_markers(content, file_path));
        issues.extend(Self::detect_long_functions(content, file_path));
        issues.extend(Self::detect_deep_nesting(content, file_path));
        issues.extend(Self::detect_many_parameters(content, file_path));
        issues.extend(Self::detect_unwrap_usage(content, file_path));
        issues.extend(Self::detect_dead_code_markers(content, file_path));
        issues.extend(Self::detect_large_file(content, file_path));

        // Sort by line number for stable, readable output.
        issues.sort_by_key(|i| i.line);
        issues
    }

    /// Generate a human-readable Markdown report from a `CodeAnalysis`.
    pub fn generate_report(&self, analysis: &CodeAnalysis) -> String {
        let mut buf = String::with_capacity(2048);

        // -- Header ---------------------------------------------------------
        buf.push_str("# Code Analysis Report\n\n");

        // -- Metrics --------------------------------------------------------
        buf.push_str("## Metrics\n\n");
        buf.push_str(&format!(
            "| Metric            | Value  |\n\
             |-------------------|--------|\n\
             | Complexity        | {:.1}  |\n\
             | Test Coverage     | {:.0}% |\n\
             | Dependency Count  | {}     |\n\n",
            analysis.metrics.complexity,
            analysis.metrics.test_coverage * 100.0,
            analysis.metrics.dependency_count,
        ));

        // -- Issue summary --------------------------------------------------
        let error_count = analysis
            .issues
            .iter()
            .filter(|i| i.severity == Severity::Error)
            .count();
        let warning_count = analysis
            .issues
            .iter()
            .filter(|i| i.severity == Severity::Warning)
            .count();
        let info_count = analysis
            .issues
            .iter()
            .filter(|i| i.severity == Severity::Info)
            .count();

        buf.push_str("## Issue Summary\n\n");
        buf.push_str(&format!(
            "- Errors: {error_count}\n- Warnings: {warning_count}\n- Info: {info_count}\n\n"
        ));

        if analysis.issues.is_empty() {
            buf.push_str("No issues detected.  Code looks clean.\n");
            return buf;
        }

        // -- Issue details --------------------------------------------------
        buf.push_str("## Issues\n\n");
        for issue in &analysis.issues {
            let severity_label = match issue.severity {
                Severity::Error => "ERROR",
                Severity::Warning => "WARN",
                Severity::Info => "INFO",
            };
            buf.push_str(&format!(
                "- **[{severity_label}]** `{}` line {}: {}\n",
                issue.file_path, issue.line, issue.message,
            ));
        }

        buf
    }

    // -- Private: metric estimators -----------------------------------------

    /// Estimate cyclomatic complexity by counting branch keywords.
    ///
    /// This is a *rough* estimate.  True cyclomatic complexity requires a
    /// control-flow graph, but keyword counting correlates well enough for a
    /// first-pass quality gate.
    fn estimate_cyclomatic_complexity(content: &str) -> f64 {
        // Start at 1 (the default path through the function).
        let mut complexity: usize = 1;

        // Branch keywords that add a decision point.
        let branch_re = Regex::new(
            r"(?m)\b(if|else\s+if|elif|match|while|for|loop|catch|except|case|\?\?|&&|\|\|)\b"
        )
        .expect("branch regex must compile");

        complexity += branch_re.find_iter(content).count();

        complexity as f64
    }

    /// Heuristic test-coverage estimate.
    ///
    /// Counts the number of `#[test]`, `#[tokio::test]`, `fn test_`, `def test_`,
    /// `it(`, `test(` markers and compares to the number of public function
    /// signatures.  Returns 0.0..1.0.
    fn estimate_test_coverage(content: &str) -> f64 {
        let test_re = Regex::new(
            r"(?m)(#\[(?:tokio::)?test\]|fn\s+test_|def\s+test_|\bit\s*\(|\btest\s*\()"
        )
        .expect("test regex must compile");

        let pub_fn_re = Regex::new(
            r"(?m)(pub\s+(?:async\s+)?fn\s|export\s+(?:async\s+)?function\s|def\s+[a-z_]\w*\s*\()"
        )
        .expect("pub fn regex must compile");

        let test_count = test_re.find_iter(content).count();
        let fn_count = pub_fn_re.find_iter(content).count();

        if fn_count == 0 {
            return 0.0;
        }

        let ratio = test_count as f64 / fn_count as f64;
        ratio.min(1.0)
    }

    // -- Private: issue detectors -------------------------------------------

    /// Detect potentially unused imports.
    ///
    /// Strategy: find all `use ...::Foo;` lines (Rust) / `import { Foo } from`
    /// (TS/JS) and check whether `Foo` appears elsewhere in the file.
    fn detect_unused_imports(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let mut issues = Vec::new();

        // Rust: `use some::path::Item;`
        let rust_use_re = Regex::new(r"(?m)^use\s+[\w:]+::(\w+);").expect("regex");
        for (line_idx, line) in content.lines().enumerate() {
            if let Some(cap) = rust_use_re.captures(line) {
                let symbol = &cap[1];
                // Search the rest of the file for this symbol (excluding the
                // import line itself).
                let rest: String = content
                    .lines()
                    .enumerate()
                    .filter(|(i, _)| *i != line_idx)
                    .map(|(_, l)| l)
                    .collect::<Vec<_>>()
                    .join("\n");

                let symbol_re = Regex::new(&format!(r"\b{}\b", regex::escape(symbol)));
                if let Ok(re) = symbol_re {
                    if !re.is_match(&rest) {
                        issues.push(AnalysisIssue {
                            file_path: file_path.to_string(),
                            line: line_idx + 1,
                            severity: Severity::Warning,
                            message: format!("Potentially unused import: `{symbol}`"),
                        });
                    }
                }
            }
        }

        // TypeScript / JavaScript: `import { Foo } from '...'`
        let ts_import_re =
            Regex::new(r"(?m)^import\s+\{([^}]+)\}\s+from\s+").expect("regex");
        for (line_idx, line) in content.lines().enumerate() {
            if let Some(cap) = ts_import_re.captures(line) {
                let symbols_str = &cap[1];
                for sym in symbols_str.split(',') {
                    let sym = sym.trim();
                    if sym.is_empty() {
                        continue;
                    }
                    // Handle `Foo as Bar`
                    let effective_sym = sym.split_whitespace().last().unwrap_or(sym);

                    let rest: String = content
                        .lines()
                        .enumerate()
                        .filter(|(i, _)| *i != line_idx)
                        .map(|(_, l)| l)
                        .collect::<Vec<_>>()
                        .join("\n");

                    let sym_re =
                        Regex::new(&format!(r"\b{}\b", regex::escape(effective_sym)));
                    if let Ok(re) = sym_re {
                        if !re.is_match(&rest) {
                            issues.push(AnalysisIssue {
                                file_path: file_path.to_string(),
                                line: line_idx + 1,
                                severity: Severity::Warning,
                                message: format!(
                                    "Potentially unused import: `{effective_sym}`"
                                ),
                            });
                        }
                    }
                }
            }
        }

        issues
    }

    /// Detect hardcoded secrets (API keys, passwords, tokens).
    fn detect_hardcoded_secrets(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let mut issues = Vec::new();

        let patterns = [
            (
                r#"(?i)(password|passwd|secret|api[_-]?key|token|auth[_-]?token)\s*=\s*["'][^"']{4,}["']"#,
                "Potential hardcoded secret",
            ),
            (
                r#"(?i)(AWS|GITHUB|STRIPE|SLACK|OPENAI)[_A-Z]*\s*=\s*["'][^"']{8,}["']"#,
                "Potential hardcoded API key / service credential",
            ),
            (
                r"-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----",
                "Private key embedded in source code",
            ),
        ];

        for (pattern_str, msg) in &patterns {
            let re = match Regex::new(pattern_str) {
                Ok(r) => r,
                Err(_) => continue,
            };
            for (line_idx, line) in content.lines().enumerate() {
                // Skip lines that are clearly test fixtures or examples.
                let trimmed = line.trim();
                if trimmed.starts_with("//") || trimmed.starts_with('#') || trimmed.starts_with("*") {
                    continue;
                }
                if re.is_match(line) {
                    issues.push(AnalysisIssue {
                        file_path: file_path.to_string(),
                        line: line_idx + 1,
                        severity: Severity::Error,
                        message: msg.to_string(),
                    });
                }
            }
        }

        issues
    }

    /// Detect TODO / FIXME / HACK / XXX markers.
    fn detect_todo_markers(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let mut issues = Vec::new();
        let re = Regex::new(r"(?i)\b(TODO|FIXME|HACK|XXX)\b").expect("regex");

        for (line_idx, line) in content.lines().enumerate() {
            if let Some(m) = re.find(line) {
                issues.push(AnalysisIssue {
                    file_path: file_path.to_string(),
                    line: line_idx + 1,
                    severity: Severity::Info,
                    message: format!(
                        "{} marker found: {}",
                        m.as_str().to_uppercase(),
                        line.trim(),
                    ),
                });
            }
        }

        issues
    }

    /// Detect long functions by counting lines between `fn`/`function`/`def`
    /// declarations and their matching closing brace / dedent.
    fn detect_long_functions(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let mut issues = Vec::new();

        // Track brace-delimited function bodies.
        let fn_start_re = Regex::new(
            r"(?m)^\s*(?:pub\s+)?(?:async\s+)?(?:fn|function|def)\s+(\w+)"
        )
        .expect("regex");

        let lines: Vec<&str> = content.lines().collect();

        for cap in fn_start_re.captures_iter(content) {
            let full_match = cap.get(0).unwrap();
            let fn_name = &cap[1];

            // Find the line number of the function start.
            let byte_offset = full_match.start();
            let start_line = content[..byte_offset].lines().count();

            // Count lines until we find the matching close brace (for brace
            // languages) or until indentation returns to the same level (for
            // Python).
            let fn_line = lines.get(start_line).unwrap_or(&"");
            let is_brace_lang = fn_line.contains('{') || {
                // Check next few lines for an opening brace.
                (start_line + 1..lines.len().min(start_line + 3))
                    .any(|i| lines.get(i).is_some_and(|l| l.contains('{')))
            };

            let body_len = if is_brace_lang {
                Self::count_brace_body_lines(&lines, start_line)
            } else {
                Self::count_indented_body_lines(&lines, start_line)
            };

            if body_len > LONG_FUNCTION_LOC_THRESHOLD {
                issues.push(AnalysisIssue {
                    file_path: file_path.to_string(),
                    line: start_line + 1,
                    severity: Severity::Warning,
                    message: format!(
                        "Function `{fn_name}` is {body_len} lines long (threshold: {LONG_FUNCTION_LOC_THRESHOLD}). Consider splitting."
                    ),
                });
            }
        }

        issues
    }

    /// Detect deeply nested blocks (excessive indentation).
    fn detect_deep_nesting(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let mut issues = Vec::new();
        let mut reported_ranges: Vec<usize> = Vec::new();

        for (line_idx, line) in content.lines().enumerate() {
            if line.trim().is_empty() {
                continue;
            }

            let indent = Self::leading_indent_level(line);
            if indent >= HIGH_NESTING_THRESHOLD {
                // Avoid flooding: only report the first line of each deep block.
                let already_reported = reported_ranges
                    .last()
                    .is_some_and(|&prev| line_idx - prev <= 2);
                if !already_reported {
                    issues.push(AnalysisIssue {
                        file_path: file_path.to_string(),
                        line: line_idx + 1,
                        severity: Severity::Warning,
                        message: format!(
                            "Deeply nested code (indent level {indent}). Consider extracting into a helper function."
                        ),
                    });
                    reported_ranges.push(line_idx);
                }
            }
        }

        issues
    }

    /// Detect functions with too many parameters.
    fn detect_many_parameters(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let mut issues = Vec::new();

        // Match function signatures and capture everything inside the parens.
        let fn_sig_re = Regex::new(
            r"(?m)(?:pub\s+)?(?:async\s+)?(?:fn|function|def)\s+(\w+)\s*\(([^)]*)\)"
        )
        .expect("regex");

        for cap in fn_sig_re.captures_iter(content) {
            let fn_name = &cap[1];
            let params_str = &cap[2];

            // Count parameters by splitting on commas.  Filter out `self` /
            // `&self` / `&mut self` for Rust methods.
            let param_count = params_str
                .split(',')
                .map(|p| p.trim())
                .filter(|p| !p.is_empty())
                .filter(|p| !p.contains("self"))
                .count();

            if param_count > MAX_RECOMMENDED_PARAMS {
                let byte_offset = cap.get(0).unwrap().start();
                let line_num = content[..byte_offset].lines().count() + 1;

                issues.push(AnalysisIssue {
                    file_path: file_path.to_string(),
                    line: line_num,
                    severity: Severity::Warning,
                    message: format!(
                        "Function `{fn_name}` has {param_count} parameters \
                         (recommended max {MAX_RECOMMENDED_PARAMS}). Consider \
                         using a config/options struct."
                    ),
                });
            }
        }

        issues
    }

    /// Detect `.unwrap()` usage in non-test code (Rust specific).
    fn detect_unwrap_usage(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        if !file_path.ends_with(".rs") {
            return Vec::new();
        }

        let mut issues = Vec::new();
        let mut in_test_module = false;

        for (line_idx, line) in content.lines().enumerate() {
            let trimmed = line.trim();

            // Track whether we are inside a #[cfg(test)] module.
            if trimmed.contains("#[cfg(test)]") {
                in_test_module = true;
            }

            if in_test_module {
                continue;
            }

            if trimmed.contains(".unwrap()")
                || trimmed.contains(".expect(")
            {
                // Skip lines that are comments.
                if trimmed.starts_with("//") || trimmed.starts_with("///") {
                    continue;
                }
                issues.push(AnalysisIssue {
                    file_path: file_path.to_string(),
                    line: line_idx + 1,
                    severity: Severity::Warning,
                    message: "Usage of `.unwrap()` / `.expect()` in non-test code. \
                              Consider proper error handling with `?` or `match`."
                        .to_string(),
                });
            }
        }

        issues
    }

    /// Detect `#[allow(dead_code)]` and similar suppression attributes.
    fn detect_dead_code_markers(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let mut issues = Vec::new();
        let re = Regex::new(r"#\[allow\(dead_code\)\]").expect("regex");

        for (line_idx, line) in content.lines().enumerate() {
            if re.is_match(line) {
                issues.push(AnalysisIssue {
                    file_path: file_path.to_string(),
                    line: line_idx + 1,
                    severity: Severity::Info,
                    message: "`#[allow(dead_code)]` suppression found. \
                              Consider removing unused code instead."
                        .to_string(),
                });
            }
        }

        issues
    }

    /// Flag excessively large files.
    fn detect_large_file(content: &str, file_path: &str) -> Vec<AnalysisIssue> {
        let line_count = content.lines().count();
        if line_count > 500 {
            vec![AnalysisIssue {
                file_path: file_path.to_string(),
                line: 1,
                severity: if line_count > 1000 {
                    Severity::Warning
                } else {
                    Severity::Info
                },
                message: format!(
                    "File is {line_count} lines long. Consider splitting into \
                     multiple modules for maintainability."
                ),
            }]
        } else {
            Vec::new()
        }
    }

    // -- Private: utility helpers --------------------------------------------

    /// Count lines in a brace-delimited function body starting from
    /// `start_line`.
    fn count_brace_body_lines(lines: &[&str], start_line: usize) -> usize {
        let mut depth = 0i32;
        let mut body_started = false;
        let mut body_lines = 0usize;

        for line in lines.iter().skip(start_line) {
            for ch in line.chars() {
                if ch == '{' {
                    depth += 1;
                    body_started = true;
                } else if ch == '}' {
                    depth -= 1;
                    if body_started && depth == 0 {
                        return body_lines;
                    }
                }
            }
            if body_started {
                body_lines += 1;
            }
        }

        body_lines
    }

    /// Count lines in a Python-style indented body starting from `start_line`.
    fn count_indented_body_lines(lines: &[&str], start_line: usize) -> usize {
        if start_line + 1 >= lines.len() {
            return 0;
        }

        let base_indent = Self::raw_indent(lines[start_line]);
        let mut body_lines = 0usize;

        for line in lines.iter().skip(start_line + 1) {
            if line.trim().is_empty() {
                body_lines += 1;
                continue;
            }
            let indent = Self::raw_indent(line);
            if indent <= base_indent {
                break;
            }
            body_lines += 1;
        }

        body_lines
    }

    /// Calculate the leading indentation level (in units of 4-space or 1-tab
    /// stops).
    fn leading_indent_level(line: &str) -> usize {
        let spaces: usize = line
            .chars()
            .take_while(|c| c.is_whitespace())
            .map(|c| if c == '\t' { 4 } else { 1 })
            .sum();
        spaces / 4
    }

    /// Raw indentation (number of leading whitespace characters).
    fn raw_indent(line: &str) -> usize {
        line.chars().take_while(|c| c.is_whitespace()).count()
    }
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Arc;

    // -- Helpers ------------------------------------------------------------

    fn next_test_id() -> u64 {
        static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
        COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
    }

    fn create_test_legolas() -> Legolas {
        let id = next_test_id();
        let tmp_dir = std::env::temp_dir().join(format!(
            "furnace_legolas_test_graph_{}_{}",
            std::process::id(), id,
        ));
        let kuzu = Arc::new(KuzuGraph::new(&tmp_dir).expect("create test KuzuGraph"));

        let tmp_db = std::env::temp_dir().join(format!(
            "furnace_legolas_test_{}_{}.db",
            std::process::id(), id,
        ));
        let sqlite = Arc::new(SqliteDb::new(&tmp_db).expect("create test SqliteDb"));

        Legolas::new(kuzu, sqlite)
    }

    fn sample_rust_code() -> &'static str {
        r#"
use std::collections::HashMap;
use std::io::Write;

pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

pub fn complex_function(input: &str) -> Result<String, String> {
    if input.is_empty() {
        return Err("empty".to_string());
    }
    for ch in input.chars() {
        if ch.is_alphabetic() {
            if ch.is_uppercase() {
                for _ in 0..10 {
                    if true {
                        // deeply nested
                        println!("deep");
                    }
                }
            }
        }
    }
    Ok(input.to_string())
}

fn helper() {
    let x = Some(42).unwrap();
}

// TODO: refactor this function
pub fn another(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32) -> i32 {
    a + b + c + d + e + f
}
"#
    }

    // -- Tests: compute_metrics ---------------------------------------------

    #[test]
    fn test_compute_metrics_simple() {
        let legolas = create_test_legolas();
        let code = "fn add(a: i32, b: i32) -> i32 { a + b }";
        let metrics = legolas.compute_metrics(code);

        // Minimal code has low complexity.
        assert!(
            metrics.complexity >= 1.0,
            "Minimum complexity is 1 (the default path)"
        );
    }

    #[test]
    fn test_compute_metrics_branchy() {
        let legolas = create_test_legolas();
        let code = r#"
fn decide(x: i32) -> &str {
    if x > 100 {
        "big"
    } else if x > 50 {
        "medium"
    } else if x > 0 {
        "small"
    } else {
        "non-positive"
    }
}
"#;
        let metrics = legolas.compute_metrics(code);

        // 1 base + 3 if/else-if branches = at least 4.
        assert!(
            metrics.complexity >= 4.0,
            "Expected complexity >= 4, got {}",
            metrics.complexity
        );
    }

    #[test]
    fn test_compute_metrics_test_coverage_heuristic() {
        let legolas = create_test_legolas();
        let code = r#"
pub fn add(a: i32, b: i32) -> i32 { a + b }
pub fn sub(a: i32, b: i32) -> i32 { a - b }

#[cfg(test)]
mod tests {
    #[test]
    fn test_add() { assert_eq!(super::add(1, 2), 3); }
}
"#;
        let metrics = legolas.compute_metrics(code);
        // 1 test function / 2 pub fns = 0.5
        assert!(
            metrics.test_coverage > 0.0,
            "Should detect at least some test coverage"
        );
    }

    // -- Tests: detect_issues -----------------------------------------------

    #[test]
    fn test_detect_unused_import() {
        let legolas = create_test_legolas();
        let code = "use std::io::Write;\n\nfn main() {}\n";
        let issues = legolas.detect_issues(code, "src/main.rs");

        let unused: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("unused import"))
            .collect();
        assert!(
            !unused.is_empty(),
            "Should detect unused import `Write`"
        );
    }

    #[test]
    fn test_detect_hardcoded_secret() {
        let legolas = create_test_legolas();
        let code = r#"let api_key = "sk-1234567890abcdef";"#;
        let issues = legolas.detect_issues(code, "src/config.rs");

        let secrets: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("secret") || i.message.contains("Secret"))
            .collect();
        assert!(
            !secrets.is_empty(),
            "Should detect hardcoded secret"
        );
    }

    #[test]
    fn test_detect_todo_marker() {
        let legolas = create_test_legolas();
        let code = "// TODO: fix this later\nfn foo() {}";
        let issues = legolas.detect_issues(code, "src/lib.rs");

        let todos: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("TODO"))
            .collect();
        assert!(!todos.is_empty(), "Should detect TODO marker");
        assert_eq!(todos[0].severity, Severity::Info);
    }

    #[test]
    fn test_detect_unwrap_in_non_test() {
        let legolas = create_test_legolas();
        let code = r#"
fn do_stuff() {
    let val = some_option().unwrap();
}
"#;
        let issues = legolas.detect_issues(code, "src/lib.rs");

        let unwraps: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("unwrap"))
            .collect();
        assert!(!unwraps.is_empty(), "Should detect .unwrap() usage");
    }

    #[test]
    fn test_unwrap_in_test_code_is_ignored() {
        let legolas = create_test_legolas();
        let code = r#"
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    #[test]
    fn test_add() {
        let result = Some(3).unwrap();
        assert_eq!(super::add(1, 2), result);
    }
}
"#;
        let issues = legolas.detect_issues(code, "src/lib.rs");

        let unwraps: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("unwrap"))
            .collect();
        assert!(
            unwraps.is_empty(),
            "Should not flag .unwrap() inside #[cfg(test)] module"
        );
    }

    #[test]
    fn test_detect_many_parameters() {
        let legolas = create_test_legolas();
        let code = "pub fn big(a: i32, b: i32, c: i32, d: i32, e: i32, f: i32) -> i32 { 0 }";
        let issues = legolas.detect_issues(code, "src/lib.rs");

        let params: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("parameters"))
            .collect();
        assert!(!params.is_empty(), "Should flag function with too many params");
    }

    #[test]
    fn test_detect_dead_code_marker() {
        let legolas = create_test_legolas();
        let code = "#[allow(dead_code)]\nfn unused() {}";
        let issues = legolas.detect_issues(code, "src/lib.rs");

        let dead: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("dead_code"))
            .collect();
        assert!(!dead.is_empty(), "Should flag #[allow(dead_code)]");
    }

    #[test]
    fn test_detect_deep_nesting() {
        let legolas = create_test_legolas();
        let code = sample_rust_code();
        let issues = legolas.detect_issues(code, "src/lib.rs");

        let deep: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("Deeply nested"))
            .collect();
        assert!(
            !deep.is_empty(),
            "Should detect deeply nested code"
        );
    }

    // -- Tests: generate_report ---------------------------------------------

    #[test]
    fn test_generate_report_clean() {
        let legolas = create_test_legolas();
        let analysis = CodeAnalysis {
            issues: vec![],
            metrics: AnalysisMetrics {
                complexity: 2.0,
                test_coverage: 0.8,
                dependency_count: 3,
            },
        };

        let report = legolas.generate_report(&analysis);
        assert!(report.contains("No issues detected"));
        assert!(report.contains("Complexity"));
    }

    #[test]
    fn test_generate_report_with_issues() {
        let legolas = create_test_legolas();
        let analysis = CodeAnalysis {
            issues: vec![
                AnalysisIssue {
                    file_path: "src/main.rs".into(),
                    line: 10,
                    severity: Severity::Error,
                    message: "Hardcoded secret".into(),
                },
                AnalysisIssue {
                    file_path: "src/main.rs".into(),
                    line: 25,
                    severity: Severity::Warning,
                    message: "Unused import".into(),
                },
            ],
            metrics: AnalysisMetrics {
                complexity: 8.0,
                test_coverage: 0.5,
                dependency_count: 2,
            },
        };

        let report = legolas.generate_report(&analysis);
        assert!(report.contains("Errors: 1"));
        assert!(report.contains("Warnings: 1"));
        assert!(report.contains("Hardcoded secret"));
        assert!(report.contains("ERROR"));
    }

    // -- Tests: analyze_file ------------------------------------------------

    #[tokio::test]
    async fn test_analyze_file_integration() {
        let legolas = create_test_legolas();
        let code = sample_rust_code();

        let analysis = legolas
            .analyze_file("src/sample.rs", code)
            .await
            .expect("analysis should succeed");

        assert!(
            analysis.metrics.complexity > 1.0,
            "Complex code should have complexity > 1"
        );
        assert!(
            !analysis.issues.is_empty(),
            "Sample code should have some issues"
        );
    }

    // -- Tests: helper functions --------------------------------------------

    #[test]
    fn test_leading_indent_level() {
        assert_eq!(Legolas::leading_indent_level("hello"), 0);
        assert_eq!(Legolas::leading_indent_level("    hello"), 1);
        assert_eq!(Legolas::leading_indent_level("        hello"), 2);
        assert_eq!(Legolas::leading_indent_level("\thello"), 1);
    }

    #[test]
    fn test_count_brace_body_lines() {
        let code = "fn foo() {\n    let x = 1;\n    let y = 2;\n}\n";
        let lines: Vec<&str> = code.lines().collect();
        let count = Legolas::count_brace_body_lines(&lines, 0);
        // Body is lines 1, 2, 3 (the closing brace line is where we stop)
        assert!(count >= 2, "Expected at least 2 body lines, got {count}");
    }

    #[test]
    fn test_issues_are_sorted_by_line() {
        let legolas = create_test_legolas();
        let code = "// TODO: first\nfn foo() {}\n// FIXME: second\n";
        let issues = legolas.detect_issues(code, "src/lib.rs");

        for pair in issues.windows(2) {
            assert!(
                pair[0].line <= pair[1].line,
                "Issues should be sorted by line number"
            );
        }
    }

    #[test]
    fn test_large_file_detection() {
        let legolas = create_test_legolas();
        let code = "fn foo() {}\n".repeat(600);
        let issues = legolas.detect_issues(&code, "src/big.rs");

        let large: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("lines long"))
            .collect();
        assert!(!large.is_empty(), "Should flag large file");
    }

    #[test]
    fn test_no_false_positive_on_small_file() {
        let legolas = create_test_legolas();
        let code = "pub fn add(a: i32, b: i32) -> i32 { a + b }\n";
        let issues = legolas.detect_issues(code, "src/lib.rs");

        let large: Vec<_> = issues
            .iter()
            .filter(|i| i.message.contains("lines long") && i.line == 1)
            .collect();
        assert!(large.is_empty(), "Small file should not be flagged as large");
    }

    // -- Tests: LLM review parsing ------------------------------------------

    #[test]
    fn test_parse_llm_review_issues_valid_json() {
        let raw = r#"[
            {"line": 10, "severity": "error", "message": "Potential null deref"},
            {"line": 25, "severity": "warning", "message": "Missing error handling"},
            {"line": 42, "severity": "info", "message": "Consider extracting helper"}
        ]"#;
        let issues = Legolas::parse_llm_review_issues(raw, "src/main.rs").unwrap();
        assert_eq!(issues.len(), 3);
        assert_eq!(issues[0].line, 10);
        assert_eq!(issues[0].severity, Severity::Error);
        assert_eq!(issues[0].message, "Potential null deref");
        assert_eq!(issues[1].severity, Severity::Warning);
        assert_eq!(issues[2].severity, Severity::Info);
        assert!(issues.iter().all(|i| i.file_path == "src/main.rs"));
    }

    #[test]
    fn test_parse_llm_review_issues_with_fences() {
        let raw = "```json\n[\n{\"line\": 5, \"severity\": \"warning\", \"message\": \"Unused variable\"}\n]\n```";
        let issues = Legolas::parse_llm_review_issues(raw, "src/lib.rs").unwrap();
        assert_eq!(issues.len(), 1);
        assert_eq!(issues[0].line, 5);
        assert_eq!(issues[0].message, "Unused variable");
    }

    #[test]
    fn test_parse_llm_review_issues_empty_array() {
        let raw = "[]";
        let issues = Legolas::parse_llm_review_issues(raw, "src/lib.rs").unwrap();
        assert!(issues.is_empty());
    }

    #[test]
    fn test_parse_llm_review_issues_invalid_json() {
        let raw = "This is not JSON at all";
        let result = Legolas::parse_llm_review_issues(raw, "src/lib.rs");
        assert!(result.is_err());
    }

    #[test]
    fn test_parse_llm_review_issues_skips_empty_messages() {
        let raw = r#"[
            {"line": 1, "severity": "error", "message": "Real issue"},
            {"line": 2, "severity": "warning", "message": ""}
        ]"#;
        let issues = Legolas::parse_llm_review_issues(raw, "src/lib.rs").unwrap();
        assert_eq!(issues.len(), 1, "Should skip entries with empty messages");
    }

    #[test]
    fn test_extract_all_code_blocks() {
        let response = "File 1 review:\n```json\n[{\"line\":1}]\n```\n\nFile 2 review:\n```json\n[{\"line\":2}]\n```\n";
        let blocks = Legolas::extract_all_code_blocks(response);
        assert_eq!(blocks.len(), 2);
        assert!(blocks[0].contains("\"line\":1"));
        assert!(blocks[1].contains("\"line\":2"));
    }

    #[test]
    fn test_extract_all_code_blocks_with_lang_tag() {
        let response = "```json\n[]\n```\n\n```\n[{\"line\":5}]\n```\n";
        let blocks = Legolas::extract_all_code_blocks(response);
        assert_eq!(blocks.len(), 2);
    }

    #[test]
    fn test_extract_all_code_blocks_skips_empty() {
        let response = "```\n\n```\n\n```json\n[{\"line\":1}]\n```\n";
        let blocks = Legolas::extract_all_code_blocks(response);
        assert_eq!(blocks.len(), 1, "Should skip empty code blocks");
    }

    #[tokio::test]
    async fn test_review_code_batch_no_llm_returns_empty() {
        let legolas = create_test_legolas(); // No LLM wired
        let files: Vec<(&str, &str)> = vec![("src/a.rs", "fn a() {}")];
        let results = legolas.review_code_batch(&files).await.unwrap();
        assert!(results.is_empty(), "No LLM should return empty results");
    }

    #[tokio::test]
    async fn test_review_code_batch_empty_files() {
        let legolas = create_test_legolas();
        let files: Vec<(&str, &str)> = vec![];
        let results = legolas.review_code_batch(&files).await.unwrap();
        assert!(results.is_empty());
    }

    #[tokio::test]
    async fn test_analyze_file_heuristic_does_not_include_llm() {
        let legolas = create_test_legolas();
        let code = sample_rust_code();
        let analysis = legolas
            .analyze_file_heuristic("src/sample.rs", code)
            .await
            .expect("heuristic analysis should succeed");

        // Should work without LLM and produce heuristic issues
        assert!(analysis.metrics.complexity > 1.0);
        assert!(!analysis.issues.is_empty());
    }

    #[test]
    fn test_prepare_review_prompts_returns_one_per_file() {
        let legolas = create_test_legolas();

        let files: Vec<(&str, &str)> = vec![
            ("src/auth.rs", "fn authenticate() { todo!() }"),
            ("src/db.rs", "fn connect() { todo!() }"),
            ("src/api.rs", "fn handler() { todo!() }"),
        ];

        let prompts = legolas.prepare_review_prompts(&files);

        assert_eq!(prompts.len(), 3);
        for (i, prompt) in prompts.iter().enumerate() {
            assert_eq!(prompt.agent, "Legolas");
            // task_id now holds the original file path for reconstruction.
            assert_eq!(prompt.task_id.as_deref(), Some(files[i].0));
            assert!(prompt.system_prompt.contains("code reviewer"));
            assert!(prompt.user_prompt.contains(files[i].0));
            assert!(prompt.user_prompt.contains(files[i].1));
            assert!(prompt.description.contains(files[i].0));
            // ID should contain the reversibly-encoded path (:: instead of /).
            assert!(prompt.id.starts_with("legolas-review-"));
        }

        // Check IDs are unique
        assert_ne!(prompts[0].id, prompts[1].id);
        assert_ne!(prompts[1].id, prompts[2].id);
    }
}
