//! Async Anti-Pattern detector - identifies async-specific code smells.
//!
//! Detects common async anti-patterns that cause performance issues:
//! 1. Blocking calls in async functions (time.sleep, requests, subprocess)
//! 2. Wasteful async - async functions with no await calls
//! 3. Sync I/O in async context (open(), input())

use std::collections::{HashMap, HashSet};

use crate::detectors::base::{Detector, DetectorConfig, DetectorResult};
use crate::graph::GraphClient;
use crate::models::{Finding, Severity};

/// Async anti-pattern detector
///
/// Uses function properties to detect:
/// - Blocking calls: time.sleep(), requests.*, subprocess.run(), etc.
/// - Wasteful async: async def with no await (unnecessary overhead)
/// - Sync I/O: open(), input() instead of aiofiles/async alternatives
pub struct AsyncAntipatternDetector {
    config: DetectorConfig,
    /// Maximum async functions without await to report
    max_findings: usize,
}

impl AsyncAntipatternDetector {
    /// Create a new async antipattern detector with default config
    pub fn new() -> Self {
        Self {
            config: DetectorConfig::default(),
            max_findings: 100,
        }
    }

    /// Create with custom max findings limit
    pub fn with_max_findings(mut self, max: usize) -> Self {
        self.max_findings = max;
        self
    }

    /// Get blocking alternatives map
    fn blocking_calls() -> HashMap<&'static str, &'static str> {
        let mut map = HashMap::new();
        // Time/sleep
        map.insert("time.sleep", "asyncio.sleep");
        map.insert("sleep", "asyncio.sleep");
        // HTTP requests
        map.insert("requests.get", "aiohttp.ClientSession.get or httpx.AsyncClient.get");
        map.insert("requests.post", "aiohttp.ClientSession.post or httpx.AsyncClient.post");
        map.insert("requests.put", "aiohttp.ClientSession.put or httpx.AsyncClient.put");
        map.insert("requests.delete", "aiohttp.ClientSession.delete or httpx.AsyncClient.delete");
        map.insert("requests.patch", "aiohttp.ClientSession.patch or httpx.AsyncClient.patch");
        map.insert("requests.request", "aiohttp or httpx async client");
        map.insert("urllib.request.urlopen", "aiohttp or httpx async client");
        // Subprocess
        map.insert("subprocess.run", "asyncio.create_subprocess_exec");
        map.insert("subprocess.call", "asyncio.create_subprocess_exec");
        map.insert("subprocess.check_output", "asyncio.create_subprocess_exec with communicate()");
        map.insert("subprocess.Popen", "asyncio.create_subprocess_exec");
        map.insert("os.system", "asyncio.create_subprocess_shell");
        // File I/O
        map.insert("open", "aiofiles.open");
        // User input
        map.insert("input", "aioconsole.ainput or async stdin reader");
        // Database
        map.insert("cursor.execute", "async database driver (asyncpg, aiomysql, aiosqlite)");
        map.insert("connection.execute", "async database driver");
        map
    }

    /// Get blocking patterns (prefix matches)
    fn blocking_patterns() -> HashMap<&'static str, &'static str> {
        let mut map = HashMap::new();
        map.insert("requests.", "aiohttp or httpx async client");
        map.insert("urllib.", "aiohttp or httpx async client");
        map.insert("subprocess.", "asyncio subprocess APIs");
        map.insert("sqlite3.", "aiosqlite");
        map.insert("psycopg2.", "asyncpg");
        map.insert("pymysql.", "aiomysql");
        map
    }

    /// Get async alternative for a blocking call
    fn get_blocking_alternative(call_name: &str) -> Option<&'static str> {
        // Check exact match
        if let Some(alt) = Self::blocking_calls().get(call_name) {
            return Some(alt);
        }

        // Check prefix patterns
        for (pattern, alt) in Self::blocking_patterns() {
            if call_name.starts_with(pattern) {
                return Some(alt);
            }
        }

        None
    }

    /// Find blocking calls in async functions
    fn find_blocking_calls_in_async(&self, graph: &GraphClient) -> anyhow::Result<Vec<Finding>> {
        let mut findings = Vec::new();

        // Query for async functions and their calls
        let query = r#"
            MATCH (f:Function)-[:CALLS]->(target)
            WHERE f.is_async = true AND target.name IS NOT NULL
            RETURN f.qualifiedName AS func_name,
                   f.name AS func_simple_name,
                   f.filePath AS func_file,
                   f.lineStart AS func_line,
                   target.name AS call_name
            ORDER BY f.qualifiedName
        "#;

        let results = graph.execute(query)?;

        // Group blocking calls by function
        let mut func_blocking_calls: HashMap<String, FunctionBlockingInfo> = HashMap::new();

        for row in results {
            let func_name = row.get_string("func_name").unwrap_or_default();
            let call_name = row.get_string("call_name").unwrap_or_default();

            if func_name.is_empty() || call_name.is_empty() {
                continue;
            }

            // Check if this is a blocking call
            if let Some(alternative) = Self::get_blocking_alternative(&call_name) {
                let entry = func_blocking_calls.entry(func_name.clone()).or_insert_with(|| {
                    FunctionBlockingInfo {
                        func_simple_name: row.get_string("func_simple_name").unwrap_or_default(),
                        func_file: row.get_string("func_file").unwrap_or_default(),
                        func_line: row.get_i64("func_line"),
                        blocking_calls: Vec::new(),
                    }
                });

                entry.blocking_calls.push(BlockingCallInfo {
                    call_name: call_name.clone(),
                    alternative: alternative.to_string(),
                });
            }
        }

        // Create findings
        for (func_name, info) in func_blocking_calls {
            if findings.len() >= self.max_findings {
                break;
            }

            let finding = self.create_blocking_call_finding(&func_name, &info);
            findings.push(finding);
        }

        Ok(findings)
    }

    /// Find async functions that never use await
    fn find_wasteful_async(&self, graph: &GraphClient) -> anyhow::Result<Vec<Finding>> {
        let mut findings = Vec::new();

        // Query for async functions with no calls to other async functions
        let query = r#"
            MATCH (f:Function)
            WHERE f.is_async = true
              AND f.has_yield = false
            OPTIONAL MATCH (f)-[:CALLS]->(called:Function)
            WHERE called.is_async = true
            WITH f, count(called) AS async_calls
            WHERE async_calls = 0
            RETURN f.qualifiedName AS func_name,
                   f.name AS func_simple_name,
                   f.filePath AS func_file,
                   f.lineStart AS func_line,
                   f.complexity AS complexity
            ORDER BY f.complexity DESC
            LIMIT 50
        "#;

        let results = graph.execute(query)?;

        for row in results {
            let func_name = row.get_string("func_name").unwrap_or_default();
            let func_simple_name = row.get_string("func_simple_name").unwrap_or_default();

            if func_name.is_empty() {
                continue;
            }

            // Skip legitimate patterns
            if Self::is_legitimate_async_without_await(&func_simple_name) {
                continue;
            }

            if findings.len() >= self.max_findings {
                break;
            }

            let finding = self.create_wasteful_async_finding(&row);
            findings.push(finding);
        }

        Ok(findings)
    }

    /// Check if function is a legitimate async without await
    fn is_legitimate_async_without_await(func_name: &str) -> bool {
        let legitimate_patterns = [
            "__aenter__",
            "__aexit__",
            "__anext__",
            "__aiter__",
            "async_generator",
            "mock_",
            "stub_",
            "fake_",
        ];

        for pattern in legitimate_patterns {
            if func_name == pattern
                || func_name.starts_with(pattern)
                || func_name.ends_with(pattern)
            {
                return true;
            }
        }

        false
    }

    fn create_blocking_call_finding(&self, func_name: &str, info: &FunctionBlockingInfo) -> Finding {
        let call_count = info.blocking_calls.len();

        // Format blocking calls for description
        let calls_display: Vec<String> = info
            .blocking_calls
            .iter()
            .take(5)
            .map(|bc| format!("- `{}` → use `{}`", bc.call_name, bc.alternative))
            .collect();

        let mut description = format!(
            "Async function `{}` calls blocking operations:\n\n{}\n\n\
             Blocking calls in async functions defeat the purpose of async/await \
             and can block the entire event loop, causing performance issues.",
            info.func_simple_name,
            calls_display.join("\n")
        );

        if call_count > 5 {
            description.push_str(&format!("\n- ... and {} more", call_count - 5));
        }

        let severity = if call_count >= 3 {
            Severity::High
        } else {
            Severity::Medium
        };

        // Build suggestion
        let mut seen_alternatives: HashSet<String> = HashSet::new();
        let mut suggestion_lines = vec!["Replace blocking calls with async alternatives:\n".to_string()];

        for bc in &info.blocking_calls {
            if !seen_alternatives.contains(&bc.alternative) {
                suggestion_lines.push(format!("- {} → {}", bc.call_name, bc.alternative));
                seen_alternatives.insert(bc.alternative.clone());
            }
        }

        let effort = if call_count >= 5 {
            "Medium (2-4 hours)"
        } else if call_count >= 2 {
            "Small (1-2 hours)"
        } else {
            "Small (30 minutes)"
        };

        Finding {
            id: format!("async_blocking_{}_{}", func_name, call_count),
            detector: "AsyncAntipatternDetector".to_string(),
            severity,
            title: format!(
                "Blocking calls in async function: {}",
                info.func_simple_name
            ),
            description,
            affected_nodes: vec![func_name.to_string()],
            affected_files: if info.func_file.is_empty() {
                vec![]
            } else {
                vec![info.func_file.clone()]
            },
            line_start: info.func_line,
            line_end: None,
            suggested_fix: Some(suggestion_lines.join("\n")),
            estimated_effort: Some(effort.to_string()),
            confidence: 0.90,
            tags: vec![
                "async_antipattern".to_string(),
                "blocking_call".to_string(),
                "performance".to_string(),
            ],
            metadata: serde_json::json!({
                "pattern_type": "blocking_call",
                "function_name": info.func_simple_name,
                "blocking_calls": info.blocking_calls.iter().map(|bc| &bc.call_name).collect::<Vec<_>>(),
                "call_count": call_count,
            }),
        }
    }

    fn create_wasteful_async_finding(&self, row: &crate::graph::QueryRow) -> Finding {
        let func_name = row.get_string("func_name").unwrap_or_default();
        let func_simple_name = row.get_string("func_simple_name").unwrap_or_default();
        let func_file = row.get_string("func_file").unwrap_or_default();
        let func_line = row.get_i64("func_line");
        let complexity = row.get_i64("complexity").unwrap_or(0);

        let description = format!(
            "Async function `{}` doesn't use `await` anywhere.\n\n\
             This function has async overhead but doesn't perform any async operations. \
             Either:\n\
             1. Remove the `async` keyword if no async operations are needed\n\
             2. Add proper async operations using `await`\n\
             3. If this is intentional (e.g., for API compatibility), add a comment",
            func_simple_name
        );

        let suggestion = format!(
            "Option 1: Remove 'async' keyword if no async operations needed:\n\
             - Change 'async def {}(...)' to 'def {}(...)'\n\n\
             Option 2: Add async operations if they should be async:\n\
             - Use 'await' for async function calls\n\
             - Use 'async for' for async iteration\n\
             - Use 'async with' for async context managers",
            func_simple_name, func_simple_name
        );

        Finding {
            id: format!("async_wasteful_{}", func_name),
            detector: "AsyncAntipatternDetector".to_string(),
            severity: Severity::Medium,
            title: format!("Wasteful async: {} has no await", func_simple_name),
            description,
            affected_nodes: vec![func_name],
            affected_files: if func_file.is_empty() {
                vec![]
            } else {
                vec![func_file]
            },
            line_start: func_line,
            line_end: None,
            suggested_fix: Some(suggestion),
            estimated_effort: Some("Small (15-30 minutes)".to_string()),
            confidence: 0.75,
            tags: vec![
                "async_antipattern".to_string(),
                "wasteful_async".to_string(),
                "code_smell".to_string(),
            ],
            metadata: serde_json::json!({
                "pattern_type": "wasteful_async",
                "function_name": func_simple_name,
                "complexity": complexity,
            }),
        }
    }
}

impl Default for AsyncAntipatternDetector {
    fn default() -> Self {
        Self::new()
    }
}

impl Detector for AsyncAntipatternDetector {
    fn name(&self) -> &'static str {
        "AsyncAntipatternDetector"
    }

    fn description(&self) -> &'static str {
        "Detects async anti-patterns: blocking calls in async functions and wasteful async (no await)"
    }

    fn detect(&self, graph: &GraphClient) -> DetectorResult {
        let mut findings = Vec::new();

        // Find blocking calls in async functions
        match self.find_blocking_calls_in_async(graph) {
            Ok(blocking_findings) => findings.extend(blocking_findings),
            Err(e) => tracing::warn!("Failed to find blocking calls: {}", e),
        }

        // Find wasteful async
        match self.find_wasteful_async(graph) {
            Ok(wasteful_findings) => findings.extend(wasteful_findings),
            Err(e) => tracing::warn!("Failed to find wasteful async: {}", e),
        }

        Ok(findings)
    }

    fn is_dependent(&self) -> bool {
        false
    }
}

/// Info about blocking calls in a function
struct FunctionBlockingInfo {
    func_simple_name: String,
    func_file: String,
    func_line: Option<i64>,
    blocking_calls: Vec<BlockingCallInfo>,
}

/// Info about a single blocking call
struct BlockingCallInfo {
    call_name: String,
    alternative: String,
}

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

    #[test]
    fn test_blocking_alternatives() {
        assert!(AsyncAntipatternDetector::get_blocking_alternative("time.sleep").is_some());
        assert!(AsyncAntipatternDetector::get_blocking_alternative("requests.get").is_some());
        assert!(AsyncAntipatternDetector::get_blocking_alternative("requests.custom").is_some());
        assert!(AsyncAntipatternDetector::get_blocking_alternative("unknown_func").is_none());
    }

    #[test]
    fn test_legitimate_async_patterns() {
        assert!(AsyncAntipatternDetector::is_legitimate_async_without_await("__aenter__"));
        assert!(AsyncAntipatternDetector::is_legitimate_async_without_await("__aexit__"));
        assert!(AsyncAntipatternDetector::is_legitimate_async_without_await("mock_something"));
        assert!(!AsyncAntipatternDetector::is_legitimate_async_without_await("regular_async"));
    }
}
