//! Truly Unused Imports Detector.
//!
//! Detects imports that are never used in any execution path, going beyond
//! traditional linters which only check syntactic usage.
//!
//! This detector uses graph analysis to trace call chains and determine if
//! imported modules are actually invoked anywhere in the code.

use std::collections::HashSet;

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

/// Truly unused imports detector
///
/// Goes beyond syntactic checking to verify imports are actually used
/// in execution paths via call graph analysis.
pub struct TrulyUnusedImportsDetector {
    config: DetectorConfig,
    /// Maximum call depth to trace
    max_call_depth: usize,
    /// Maximum findings to report
    max_findings: usize,
}

impl TrulyUnusedImportsDetector {
    /// Create a new truly unused imports detector
    pub fn new() -> Self {
        Self {
            config: DetectorConfig::default(),
            max_call_depth: 3,
            max_findings: 100,
        }
    }

    /// Set max call depth
    pub fn with_max_call_depth(mut self, depth: usize) -> Self {
        self.max_call_depth = depth;
        self
    }

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

    /// Find truly unused imports via graph analysis
    fn find_unused_imports(&self, graph: &GraphClient) -> anyhow::Result<Vec<Finding>> {
        // Step 1: Get all imports from non-test files
        let imports_query = r#"
            MATCH (f:File)-[imp:IMPORTS]->(m)
            WHERE ('Module' IN labels(m) OR 'Class' IN labels(m) OR 'Function' IN labels(m))
            RETURN DISTINCT f.filePath as file_path,
                   m.qualifiedName as import_qname,
                   m.name as import_name,
                   labels(m)[0] as import_type
            ORDER BY f.filePath, m.name
        "#;

        let all_imports = graph.execute(imports_query)?;

        if all_imports.is_empty() {
            return Ok(Vec::new());
        }

        // Step 2: Check each import for usage
        let unused_imports = self.find_unused_imports_cypher(graph, &all_imports)?;

        // Step 3: Group by file for better reporting
        let mut imports_by_file: std::collections::HashMap<String, Vec<ImportInfo>> =
            std::collections::HashMap::new();

        for imp in unused_imports {
            imports_by_file
                .entry(imp.file_path.clone())
                .or_default()
                .push(imp);
        }

        // Create findings
        let mut findings = Vec::new();

        for (file_path, imports) in imports_by_file {
            if findings.len() >= self.max_findings {
                break;
            }

            let finding = self.create_finding(&file_path, &imports);
            findings.push(finding);
        }

        Ok(findings)
    }

    fn find_unused_imports_cypher(
        &self,
        graph: &GraphClient,
        all_imports: &[crate::graph::QueryRow],
    ) -> anyhow::Result<Vec<ImportInfo>> {
        let mut unused = Vec::new();

        for imp in all_imports {
            let file_path = imp.get_string("file_path").unwrap_or_default();
            let import_qname = imp.get_string("import_qname").unwrap_or_default();
            let import_name = imp.get_string("import_name").unwrap_or_default();
            let import_type = imp.get_string("import_type").unwrap_or_default();

            if !self.is_import_used(graph, &file_path, &import_qname, &import_name)? {
                unused.push(ImportInfo {
                    file_path,
                    qualified_name: import_qname,
                    name: import_name,
                    import_type,
                });

                if unused.len() >= self.max_findings * 10 {
                    // Stop early if we have way more than needed
                    break;
                }
            }
        }

        Ok(unused)
    }

    fn is_import_used(
        &self,
        graph: &GraphClient,
        file_path: &str,
        import_qname: &str,
        import_name: &str,
    ) -> anyhow::Result<bool> {
        // Check 1: Used in call chains
        let call_chain_query = format!(
            r#"
            MATCH (f:File {{filePath: $file_path}})-[:CONTAINS*]->(func:Function)
            MATCH path = (func)-[:CALLS*1..{}]->(m)
            WHERE m.qualifiedName = $import_qname
            RETURN 1 AS used LIMIT 1
            "#,
            self.max_call_depth
        );

        let results = graph.execute_with_params(
            &call_chain_query,
            vec![
                ("file_path", file_path.into()),
                ("import_qname", import_qname.into()),
            ],
        )?;

        if !results.is_empty() {
            return Ok(true);
        }

        // Check 2: Used directly
        let direct_use_query = r#"
            MATCH (f:File {filePath: $file_path})-[:CONTAINS*]->(func:Function)
            MATCH (func)-[:USES]->(m)
            WHERE m.qualifiedName = $import_qname
            RETURN 1 AS used LIMIT 1
        "#;

        let results = graph.execute_with_params(
            direct_use_query,
            vec![
                ("file_path", file_path.into()),
                ("import_qname", import_qname.into()),
            ],
        )?;

        if !results.is_empty() {
            return Ok(true);
        }

        // Check 3: Used via inheritance
        let inheritance_query = r#"
            MATCH (f:File {filePath: $file_path})-[:CONTAINS*]->(c:Class)
            MATCH (c)-[:INHERITS]->(m)
            WHERE m.qualifiedName = $import_qname
            RETURN 1 AS used LIMIT 1
        "#;

        let results = graph.execute_with_params(
            inheritance_query,
            vec![
                ("file_path", file_path.into()),
                ("import_qname", import_qname.into()),
            ],
        )?;

        if !results.is_empty() {
            return Ok(true);
        }

        // Check 4: Used in decorators
        let decorator_query = r#"
            MATCH (f:File {filePath: $file_path})-[:CONTAINS*]->(node)
            WHERE (node:Function OR node:Class)
              AND node.decorators IS NOT NULL
              AND ANY(decorator IN node.decorators WHERE decorator STARTS WITH $import_prefix)
            RETURN 1 AS used LIMIT 1
        "#;

        let import_prefix = format!("{}.", import_name);
        let results = graph.execute_with_params(
            decorator_query,
            vec![
                ("file_path", file_path.into()),
                ("import_prefix", import_prefix.into()),
            ],
        )?;

        Ok(!results.is_empty())
    }

    fn create_finding(&self, file_path: &str, imports: &[ImportInfo]) -> Finding {
        let count = imports.len();

        let import_list: Vec<String> = imports
            .iter()
            .map(|imp| format!("  • {} ({})", imp.name, imp.import_type))
            .collect();

        let severity = if count >= 5 {
            Severity::Medium
        } else {
            Severity::Low
        };

        // Create suggested fixes
        let suggestions: Vec<String> = imports
            .iter()
            .take(5)
            .map(|imp| {
                if imp.import_type == "Module" {
                    format!("Remove: import {} (never called in execution paths)", imp.name)
                } else {
                    format!("Remove: from ... import {} (never used)", imp.name)
                }
            })
            .collect();

        let mut suggestion_text = suggestions.join("\n");
        if imports.len() > 5 {
            suggestion_text.push_str(&format!("\n... and {} more", imports.len() - 5));
        }

        let file_name = file_path.split('/').last().unwrap_or(file_path);

        Finding {
            id: format!("truly_unused_imports_{}", file_path.replace('/', "_")),
            detector: "TrulyUnusedImportsDetector".to_string(),
            severity,
            title: format!("Truly Unused Imports in {}", file_name),
            description: format!(
                "File '{}' has {} import(s) that are never used in any \
                 execution path (up to {} levels deep in the call graph):\n\n{}\n\n\
                 Unlike traditional linters that check syntactic usage, this detector \
                 uses graph analysis to verify that imports are actually invoked. \
                 These imports may be referenced in code but are never executed.",
                file_path,
                count,
                self.max_call_depth,
                import_list.join("\n")
            ),
            affected_nodes: imports.iter().map(|i| i.qualified_name.clone()).collect(),
            affected_files: vec![file_path.to_string()],
            line_start: None,
            line_end: None,
            suggested_fix: Some(suggestion_text),
            estimated_effort: Some("Small (5-15 minutes)".to_string()),
            confidence: 0.85,
            tags: vec![
                "unused_import".to_string(),
                "dead_code".to_string(),
                "execution_path".to_string(),
            ],
            metadata: serde_json::json!({
                "unused_imports": imports.iter().map(|i| serde_json::json!({
                    "name": i.name,
                    "qualified_name": i.qualified_name,
                    "type": i.import_type,
                })).collect::<Vec<_>>(),
                "count": count,
                "max_call_depth": self.max_call_depth,
            }),
        }
    }
}

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

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

    fn description(&self) -> &'static str {
        "Detects imports never used in execution paths via call graph analysis"
    }

    fn detect(&self, graph: &GraphClient) -> DetectorResult {
        self.find_unused_imports(graph)
    }

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

/// Import information
struct ImportInfo {
    file_path: String,
    qualified_name: String,
    name: String,
    import_type: String,
}

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

    #[test]
    fn test_detector_creation() {
        let detector = TrulyUnusedImportsDetector::new();
        assert_eq!(detector.name(), "TrulyUnusedImportsDetector");
        assert_eq!(detector.max_call_depth, 3);
    }

    #[test]
    fn test_with_options() {
        let detector = TrulyUnusedImportsDetector::new()
            .with_max_call_depth(5)
            .with_max_findings(50);
        assert_eq!(detector.max_call_depth, 5);
        assert_eq!(detector.max_findings, 50);
    }
}
